diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f4ea099d9c8..ebf6f614df3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "dockerfile": "Dockerfile", "args": { // Update the VARIANT arg to pick a version of Go - "VARIANT": "1.19", + "VARIANT": "1.20", // Options "INSTALL_NODE": "false", "NODE_VERSION": "lts/*" diff --git a/.github/workflows/adapter-code-coverage.yml b/.github/workflows/adapter-code-coverage.yml new file mode 100644 index 00000000000..5b5dc9331e5 --- /dev/null +++ b/.github/workflows/adapter-code-coverage.yml @@ -0,0 +1,107 @@ +name: Adapter code coverage +on: + pull_request_target: + paths: ["adapters/*/*.go"] +permissions: + pull-requests: write + contents: write +jobs: + run-coverage: + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.5 + + - name: Checkout pull request branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Get adapter directories + id: get_directories + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + const utils = require('./.github/workflows/helpers/pull-request-utils.js') + function directoryExtractor(filepath) { + // extract directory name from filepath of the form adapters//*.go + if (filepath.startsWith("adapters/") && filepath.split("/").length > 2) { + return filepath.split("/")[1] + } + return "" + } + const helper = utils.diffHelper({github, context}) + const files = await helper.getDirectories(directoryExtractor) + return files.length == 0 ? "" : JSON.stringify(files); + + - name: Run coverage tests + id: run_coverage + if: ${{ steps.get_directories.outputs.result }} != "" + run: | + directories=$(echo '${{ steps.get_directories.outputs.result }}' | jq -r '.[]') + go mod download + + # create a temporary directory to store the coverage output + temp_dir=$(mktemp -d) + touch ${temp_dir}/coverage_output.txt + + # generate coverage for adapter + cd ./adapters + for directory in $directories; do + cd $directory + coverage_profile_path="${PWD}/${directory}.out" + go test -coverprofile="${coverage_profile_path}" + go tool cover -html="${coverage_profile_path}" -o "${temp_dir}/${directory}.html" + go tool cover -func="${coverage_profile_path}" -o "${temp_dir}/${directory}.txt" + cd .. + done + echo "coverage_dir=${temp_dir}" >> $GITHUB_OUTPUT + + # remove pull request branch files + cd .. + rm -f -r ./* + + - name: Checkout coverage-preview branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: coverage-preview + repository: prebid/prebid-server + + - name: Commit coverage files to coverage-preview branch + if: ${{ steps.run_coverage.outputs.coverage_dir }} != "" + id: commit_coverage + run: | + directory=.github/preview/${{ github.run_id }}_$(date +%s) + mkdir -p $directory + cp -r ${{ steps.run_coverage.outputs.coverage_dir }}/*.html ./$directory + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add $directory/* + git commit -m 'Add coverage files' + git push origin coverage-preview + echo "remote_coverage_preview_dir=${directory}" >> $GITHUB_OUTPUT + + - name: Checkout master branch + if: ${{ steps.get_directories.outputs.result }} != "" + run: git checkout master + + - name: Add coverage summary to pull request + if: ${{ steps.run_coverage.outputs.coverage_dir }} != "" && ${{ steps.commit_coverage.outputs.remote_coverage_preview_dir }} != "" + uses: actions/github-script@v6 + with: + script: | + const utils = require('./.github/workflows/helpers/pull-request-utils.js') + const helper = utils.coverageHelper({ + github, context, + headSha: '${{ github.event.pull_request.head.sha }}', + tmpCoverageDir: '${{ steps.run_coverage.outputs.coverage_dir }}', + remoteCoverageDir: '${{ steps.commit_coverage.outputs.remote_coverage_preview_dir }}' + }) + const adapterDirectories = JSON.parse('${{ steps.get_directories.outputs.result }}') + await helper.AddCoverageSummary(adapterDirectories) diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js new file mode 100644 index 00000000000..78ab0bb2bc5 --- /dev/null +++ b/.github/workflows/helpers/pull-request-utils.js @@ -0,0 +1,414 @@ +const synchronizeEvent = "synchronize", + openedEvent = "opened", + completedStatus = "completed", + resultSize = 100 + +class diffHelper { + constructor(input) { + this.owner = input.context.repo.owner + this.repo = input.context.repo.repo + this.github = input.github + this.pullRequestNumber = input.context.payload.pull_request.number + this.pullRequestEvent = input.event + this.testName = input.testName + this.fileNameFilter = !input.fileNameFilter ? () => true : input.fileNameFilter + this.fileLineFilter = !input.fileLineFilter ? () => true : input.fileLineFilter + } + + /* + Checks whether the test defined by this.testName has been executed on the given commit + @param {string} commit - commit SHA to check for test execution + @returns {boolean} - returns true if the test has been executed on the commit, otherwise false + */ + async #isTestExecutedOnCommit(commit) { + const response = await this.github.rest.checks.listForRef({ + owner: this.owner, + repo: this.repo, + ref: commit, + }) + + return response.data.check_runs.some( + ({ status, name }) => status === completedStatus && name === this.testName + ) + } + + /* + Retrieves the line numbers of added or updated lines in the provided files + @param {Array} files - array of files containing their filename and patch + @returns {Object} - object mapping filenames to arrays of line numbers indicating the added or updated lines + */ + async #getDiffForFiles(files = []) { + let diff = {} + for (const { filename, patch } of files) { + if (this.fileNameFilter(filename)) { + const lines = patch.split("\n") + if (lines.length === 1) { + continue + } + + let lineNumber + for (const line of lines) { + // Check if line is diff header + // example: + // @@ -1,3 +1,3 @@ + // 1 var a + // 2 + // 3 - //test + // 3 +var b + // Here @@ -1,3 +1,3 @@ is diff header + if (line.match(/@@\s.*?@@/) != null) { + lineNumber = parseInt(line.match(/\+(\d+)/)[0]) + continue + } + + // "-" prefix indicates line was deleted. So do not consider deleted line + if (line.startsWith("-")) { + continue + } + + // "+"" prefix indicates line was added or updated. Include line number in diff details + if (line.startsWith("+") && this.fileLineFilter(line)) { + diff[filename] = diff[filename] || [] + diff[filename].push(lineNumber) + } + lineNumber++ + } + } + } + return diff + } + + /* + Retrieves a list of commits that have not been checked by the test defined by this.testName + @returns {Array} - array of commit SHAs that have not been checked by the test + */ + async #getNonScannedCommits() { + const { data } = await this.github.rest.pulls.listCommits({ + owner: this.owner, + repo: this.repo, + pull_number: this.pullRequestNumber, + per_page: resultSize, + }) + let nonScannedCommits = [] + + // API returns commits in ascending order. Loop in reverse to quickly retrieve unchecked commits + for (let i = data.length - 1; i >= 0; i--) { + const { sha, parents } = data[i] + + // Commit can be merged master commit. Such commit have multiple parents + // Do not consider such commit for building file diff + if (parents.length > 1) { + continue + } + + const isTestExecuted = await this.#isTestExecutedOnCommit(sha) + if (isTestExecuted) { + // Remaining commits have been tested in previous scans. Therefore, do not need to be considered again + break + } else { + nonScannedCommits.push(sha) + } + } + + // Reverse to return commits in ascending order. This is needed to build diff for commits in chronological order + return nonScannedCommits.reverse() + } + + /* + Filters the commit diff to include only the files that are part of the PR diff + @param {Array} commitDiff - array of line numbers representing lines added or updated in the commit + @param {Array} prDiff - array of line numbers representing lines added or updated in the pull request + @returns {Array} - filtered commit diff, including only the files that are part of the PR diff + */ + async #filterCommitDiff(commitDiff = [], prDiff = []) { + return commitDiff.filter((file) => prDiff.includes(file)) + } + + /* + Builds the diff for the pull request, including both the changes in the pull request and the changes in non-scanned commits + @returns {string} - json string representation of the pull request diff and the diff for non-scanned commits + */ + async buildDiff() { + const { data } = await this.github.rest.pulls.listFiles({ + owner: this.owner, + repo: this.repo, + pull_number: this.pullRequestNumber, + per_page: resultSize, + }) + + const pullRequestDiff = await this.#getDiffForFiles(data) + + const nonScannedCommitsDiff = + Object.keys(pullRequestDiff).length != 0 && this.pullRequestEvent === synchronizeEvent // The "synchronize" event implies that new commit are pushed after the pull request was opened + ? await this.getNonScannedCommitDiff(pullRequestDiff) + : {} + + const prDiffFiles = Object.keys(pullRequestDiff) + const pullRequest = { + hasChanges: prDiffFiles.length > 0, + files: prDiffFiles.join(" "), + diff: pullRequestDiff, + } + const uncheckedCommits = { diff: nonScannedCommitsDiff } + return JSON.stringify({ pullRequest, uncheckedCommits }) + } + + /* + Retrieves the diff for non-scanned commits by comparing their changes with the pull request diff + @param {Object} pullRequestDiff - The diff of files in the pull request + @returns {Object} - The diff of files in the non-scanned commits that are part of the pull request diff + */ + async getNonScannedCommitDiff(pullRequestDiff) { + let nonScannedCommitsDiff = {} + // Retrieves list of commits that have not been scanned by the PR check + const nonScannedCommits = await this.#getNonScannedCommits() + for (const commit of nonScannedCommits) { + const { data } = await this.github.rest.repos.getCommit({ + owner: this.owner, + repo: this.repo, + ref: commit, + }) + + const commitDiff = await this.#getDiffForFiles(data.files) + const files = Object.keys(commitDiff) + for (const file of files) { + // Consider scenario where the changes made to a file in the initial commit are completely undone by subsequent commits + // In such cases, the modifications from the initial commit should not be taken into account + // If the changes were entirely removed, there should be no entry for the file in the pullRequestStats + const filePRDiff = pullRequestDiff[file] + if (!filePRDiff) { + continue + } + + // Consider scenario where changes made in the commit were partially removed or modified by subsequent commits + // In such cases, include only those commit changes that are part of the pullRequestStats object + // This ensures that only the changes that are reflected in the pull request are considered + const changes = await this.#filterCommitDiff(commitDiff[file], filePRDiff) + + if (changes.length !== 0) { + // Check if nonScannedCommitsDiff[file] exists, if not assign an empty array to it + nonScannedCommitsDiff[file] = nonScannedCommitsDiff[file] || [] + // Combine the existing nonScannedCommitsDiff[file] array with the commit changes + // Remove any duplicate elements using the Set data structure + nonScannedCommitsDiff[file] = [ + ...new Set([...nonScannedCommitsDiff[file], ...changes]), + ] + } + } + } + return nonScannedCommitsDiff + } + + /* + Retrieves a list of directories from GitHub pull request files + @param {Function} directoryExtractor - The function used to extract the directory name from the filename + @returns {Array} An array of unique directory names + */ + async getDirectories(directoryExtractor = () => "") { + const { data } = await this.github.rest.pulls.listFiles({ + owner: this.owner, + repo: this.repo, + pull_number: this.pullRequestNumber, + per_page: resultSize, + }) + + const directories = [] + for (const { filename } of data) { + const directory = directoryExtractor(filename) + if (directory != "" && !directories.includes(directory)) { + directories.push(directory) + } + } + return directories + } +} + +class semgrepHelper { + constructor(input) { + this.owner = input.context.repo.owner + this.repo = input.context.repo.repo + this.github = input.github + + this.pullRequestNumber = input.context.payload.pull_request.number + this.pullRequestEvent = input.event + + this.pullRequestDiff = input.diff.pullRequest.diff + this.newCommitsDiff = input.diff.uncheckedCommits.diff + + this.semgrepErrors = [] + this.semgrepWarnings = [] + input.semgrepResult.forEach((res) => { + res.severity === "High" ? this.semgrepErrors.push(res) : this.semgrepWarnings.push(res) + }) + + this.headSha = input.headSha + } + + /* + Retrieves the matching line number from the provided diff for a given file and range of lines + @param {Object} range - object containing the file, start line, and end line to find a match + @param {Object} diff - object containing file changes and corresponding line numbers + @returns {number|null} - line number that matches the range within the diff, or null if no match is found + */ + async #getMatchingLineFromDiff({ file, start, end }, diff) { + const fileDiff = diff[file] + if (!fileDiff) { + return null + } + if (fileDiff.includes(start)) { + return start + } + if (fileDiff.includes(end)) { + return end + } + return null + } + + /* + Splits the semgrep results into different categories based on the scan + @param {Array} semgrepResults - array of results reported by semgrep + @returns {Object} - object containing the categorized semgrep results i.e results reported in previous scans and new results found in the current scan + */ + async #splitSemgrepResultsByScan(semgrepResults = []) { + const result = { + nonDiff: [], // Errors or warnings found in files updated in pull request, but not part of sections that were modified in the pull request + previous: [], // Errors or warnings found in previous semgrep scans + current: [], // Errors or warnings found in current semgrep scan + } + + for (const se of semgrepResults) { + const prDiffLine = await this.#getMatchingLineFromDiff(se, this.pullRequestDiff) + if (!prDiffLine) { + result.nonDiff.push({ ...se }) + continue + } + + switch (this.pullRequestEvent) { + case openedEvent: + // "Opened" event implies that this is the first check + // Therefore, the error should be appended to the result.current + result.current.push({ ...se, line: prDiffLine }) + case synchronizeEvent: + const commitDiffLine = await this.#getMatchingLineFromDiff(se, this.newCommitsDiff) + // Check if error or warning is part of current commit diff + // If not then error or warning was reported in previous scans + commitDiffLine != null + ? result.current.push({ ...se, line: commitDiffLine }) + : result.previous.push({ + ...se, + line: prDiffLine, + }) + } + } + return result + } + + /* + Adds review comments based on the semgrep results to the current pull request + @returns {Object} - object containing the count of unaddressed comments from the previous scan and the count of new comments from the current scan + */ + async addReviewComments() { + let result = { + previousScan: { unAddressedComments: 0 }, + currentScan: { newComments: 0 }, + } + + if (this.semgrepErrors.length == 0 && this.semgrepWarnings.length == 0) { + return result + } + + const errors = await this.#splitSemgrepResultsByScan(this.semgrepErrors) + if (errors.previous.length == 0 && errors.current.length == 0) { + console.log("Semgrep did not find any errors in the current pull request changes") + } else { + for (const { message, file, line } of errors.current) { + await this.github.rest.pulls.createReviewComment({ + owner: this.owner, + repo: this.repo, + pull_number: this.pullRequestNumber, + commit_id: this.headSha, + body: message, + path: file, + line: line, + }) + } + result.currentScan.newComments = errors.current.length + if (this.pullRequestEvent == synchronizeEvent) { + result.previousScan.unAddressedComments = errors.previous.length + } + } + + const warnings = await this.#splitSemgrepResultsByScan(this.semgrepWarnings) + for (const { message, file, line } of warnings.current) { + await this.github.rest.pulls.createReviewComment({ + owner: this.owner, + repo: this.repo, + pull_number: this.pullRequestNumber, + commit_id: this.headSha, + body: "Consider this as a suggestion. " + message, + path: file, + line: line, + }) + } + return result + } +} + +class coverageHelper { + constructor(input) { + this.owner = input.context.repo.owner + this.repo = input.context.repo.repo + this.github = input.github + this.pullRequestNumber = input.context.payload.pull_request.number + this.headSha = input.headSha + this.previewBaseURL = `https://htmlpreview.github.io/?https://github.com/${this.owner}/${this.repo}/coverage-preview/${input.remoteCoverageDir}` + this.tmpCoverDir = input.tmpCoverageDir + } + + /* + Adds a code coverage summary along with heatmap links and coverage data on pull request as comment + @param {Array} directories - directory for which coverage summary will be added + */ + async AddCoverageSummary(directories = []) { + const fs = require("fs") + const path = require("path") + const { promisify } = require("util") + const readFileAsync = promisify(fs.readFile) + + let body = "## Code coverage summary \n" + body += "Note: \n" + body += + "- Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors \n" + body += `- Coverage summary encompasses all commits leading up to the latest one, ${this.headSha} \n` + + for (const directory of directories) { + let url = `${this.previewBaseURL}/${directory}.html` + try { + const textFilePath = path.join(this.tmpCoverDir, `${directory}.txt`) + const data = await readFileAsync(textFilePath, "utf8") + + body += `#### ${directory} \n` + body += `Refer [here](${url}) for heat map coverage report \n` + body += "\`\`\` \n" + body += data + body += "\n \`\`\` \n" + } catch (err) { + console.error(err) + return + } + } + + await this.github.rest.issues.createComment({ + owner: this.owner, + repo: this.repo, + issue_number: this.pullRequestNumber, + body: body, + }) + } +} + +module.exports = { + diffHelper: (input) => new diffHelper(input), + semgrepHelper: (input) => new semgrepHelper(input), + coverageHelper: (input) => new coverageHelper(input), +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52c2c497e5b..6ea29bd3d61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,28 +1,126 @@ name: Release on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + releaseType: + type: choice + options: + - minor + - patch + default: minor + required: true + description: 'minor: v0.X.0, patch: v0.0.X' + debug: + type: boolean + default: true + description: 'executes the workflow in debug mode (skip the publishing tag, docker image and release steps)' jobs: - release: - name: Create Release - if: github.event.base_ref == 'refs/heads/master' - runs-on: ubuntu-20.04 + check-permission: + name: Check permission + if: contains(github.ref, 'refs/heads/master') + runs-on: ubuntu-latest + permissions: + contents: read steps: - - name: Get Version - id: get_version + - name: Check user permission + uses: actions-cool/check-user-permission@v2.2.0 + id: check + with: + require: 'write' + outputs: + hasWritePermission: ${{ steps.check.outputs.require-result }} + + publish-tag: + name: Publish tag + needs: check-permission + if: contains(needs.check-permission.outputs.hasWritePermission, 'true') + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout Prebid Server + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Create & publish tag + id: release + run: | + currentTag=$(git describe --abbrev=0 --tags) + echo "Current release tag ${currentTag}" + + echo ${currentTag} | grep -q "^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$" + if [ $? -ne 0 ]; then + echo "Current tag format won't let us compute the new tag name. Required format v[0-9]\+\.[0-9]\+\.[0-9]\+" + exit 1 + fi + + if [[ "${currentTag:0:1}" != "v" ]]; then + currentTag="v${currentTag}" + fi + + nextTag='' + releaseType=${{ inputs.releaseType }} + if [ $releaseType == "minor" ]; then + # increment minor version and reset patch version + nextTag=$(echo "${currentTag}" | awk -F. '{OFS="."; $2+=1; $3=0; print $0}') + else + # increment patch version + nextTag=$(echo "${currentTag}" | awk -F. '{OFS="."; $3+=1; print $0}') + fi + + if [ ${{ inputs.debug }} == 'true' ]; then + echo "running workflow in debug mode, next ${releaseType} tag: ${nextTag}" + else + git tag $nextTag + git push origin $nextTag + echo "tag=${nextTag}" >> $GITHUB_OUTPUT + fi + outputs: + releaseTag: ${{ steps.release.outputs.tag }} + + publish-docker-image: + name: Publish docker image + needs: publish-tag + if: contains(inputs.debug, 'false') + runs-on: ubuntu-latest + steps: + - name: Checkout Prebid Server + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Build image run: | - echo "tag=${GITHUB_REF/refs\/tags\/}" >> $GITHUB_OUTPUT - echo "version=${GITHUB_REF/refs\/tags\/v}" >> $GITHUB_OUTPUT + docker build -t docker.io/prebid/prebid-server:${{ needs.publish-tag.outputs.releaseTag }} . + - name: Login to docker Hub + if: contains(inputs.debug, 'false') + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Publish to docker Hub + run: | + docker push docker.io/prebid/prebid-server:${{ needs.publish-tag.outputs.releaseTag }} - - name: Create & Publish Release - uses: release-drafter/release-drafter@v5.12.1 + publish-release: + name: Publish release + needs: [publish-tag, publish-docker-image] + if: contains(inputs.debug, 'false') + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout Prebid Server + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Create & publish release + uses: release-drafter/release-drafter@v5.22.0 with: - name: ${{ steps.get_version.outputs.version }} - tag: ${{ steps.get_version.outputs.tag }} - version: ${{ steps.get_version.outputs.version }} + name: ${{ needs.publish-tag.outputs.releaseTag }} + tag: ${{ needs.publish-tag.outputs.releaseTag }} + version: ${{ needs.publish-tag.outputs.releaseTag }} publish: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000000..565bb9b871a --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,77 @@ +name: Adapter semgrep checks +on: + pull_request_target: + paths: ["adapters/*/*.go"] +permissions: + pull-requests: write +jobs: + semgrep-check: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Calculate diff + id: calculate_diff + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + const utils = require('./.github/workflows/helpers/pull-request-utils.js') + // consider only non-test Go files that are part of the adapter code + function fileNameFilter(filename) { + return filename.startsWith("adapters/") && filename.split("/").length > 2 && filename.endsWith(".go") && !filename.endsWith("_test.go") + } + const helper = utils.diffHelper({github, context, fileNameFilter, event: "${{github.event.action}}", testName: "${{github.job}}"}) + return await helper.buildDiff() + + - name: Should run semgrep + id: should_run_semgrep + run: | + hasChanges=$(echo '${{ steps.calculate_diff.outputs.result }}' | jq .pullRequest.hasChanges) + echo "hasChanges=${hasChanges}" >> $GITHUB_OUTPUT + + - name: Install semgrep + if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true') + run: | + pip3 install semgrep==1.22.0 + semgrep --version + + - name: Run semgrep tests + id: run_semgrep_tests + if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true') + run: | + unqouted_string=$(echo '${{ steps.calculate_diff.outputs.result }}' | jq .pullRequest.files | tr -d '"') + outputs=$(semgrep --gitlab-sast --config=.semgrep/adapter $unqouted_string | jq '[.vulnerabilities[] | {"file": .location.file, "severity": .severity, "start": .location.start_line, "end": .location.end_line, "message": (.message | gsub("\\n"; "\n"))}]' | jq -c | jq -R) + echo "semgrep_result=${outputs}" >> "$GITHUB_OUTPUT" + + - name: Add pull request comment + id: add_pull_request_comment + if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true') + uses: actions/github-script@v6.4.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const utils = require('./.github/workflows/helpers/pull-request-utils.js') + const helper = utils.semgrepHelper({ + github, context, event: "${{github.event.action}}", + semgrepResult: JSON.parse(${{ steps.run_semgrep_tests.outputs.semgrep_result }}), + diff: ${{ steps.calculate_diff.outputs.result }}, headSha: "${{github.event.pull_request.head.sha}}" + }) + const { previousScan, currentScan } = await helper.addReviewComments() + return previousScan.unAddressedComments + currentScan.newComments + + - name: Adapter semgrep checks result + if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true') + run: | + if [ "${{steps.add_pull_request_comment.outputs.result}}" -ne "0" ]; then + echo 'Semgrep has found "${{steps.add_pull_request_comment.outputs.result}}" errors' + exit 1 + else + echo 'Semgrep did not find any errors in the pull request changes' + fi diff --git a/.github/workflows/validate-merge.yml b/.github/workflows/validate-merge.yml index e85dc3de50c..07f1bacaa45 100644 --- a/.github/workflows/validate-merge.yml +++ b/.github/workflows/validate-merge.yml @@ -12,7 +12,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: 1.19.2 + go-version: 1.20.5 - name: Checkout Merged Branch uses: actions/checkout@v3 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 83b29709bd4..9047e1f468f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,7 +10,7 @@ jobs: validate: strategy: matrix: - go-version: [1.18.x, 1.19.x] + go-version: [1.19.x, 1.20.x] os: [ubuntu-20.04] runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 514e217b46c..4e298f2a3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,8 @@ analytics/filesystem/testFiles/ # autogenerated mac file .DS_Store + +# Autogenerated Vim swap files +*~ +*.swp +*.swo diff --git a/.semgrep/README.md b/.semgrep/README.md new file mode 100644 index 00000000000..cd19e4bcc61 --- /dev/null +++ b/.semgrep/README.md @@ -0,0 +1,17 @@ +# Semgrep Test + +Running semgrep unit tests: +```bash +semgrep --test +``` + + +Running single semgrep rules against adapter code: +```bash +semgrep --config=./adapter/{rule}.yml ../adapters/ +``` + +Running all semgrep rules simultaneously: +```bash +semgrep --config=./adapter ../adapters/ +``` diff --git a/.semgrep/adapter/bid-type-if-check.go b/.semgrep/adapter/bid-type-if-check.go new file mode 100644 index 00000000000..52b5d551377 --- /dev/null +++ b/.semgrep/adapter/bid-type-if-check.go @@ -0,0 +1,55 @@ +/* + bid-type-if-check tests + https://semgrep.dev/docs/writing-rules/testing-rules + "ruleid" prefix in comment indicates patterns that should be flagged by semgrep + "ok" prefix in comment indidcates patterns that should not be flagged by the semgrep +*/ + +func getMediaTypeForImp(impID string, imps []openrtb2.Imp) openrtb_ext.BidType { + for _, imp := range imps { + if imp.ID == impID { + // ruleid: bid-type-if-check + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + // ruleid: bid-type-if-check + } else if imp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + // ruleid: bid-type-if-check + } else if imp.Native != nil { + return openrtb_ext.BidTypeNative, nil + // ruleid: bid-type-if-check + } else if imp.Audio != nil { + return openrtb_ext.BidTypeAudio, nil + } + } + } + return openrtb_ext.BidTypeBanner +} + +func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { + for _, imp := range imps { + if imp.ID == impID { + // ruleid: bid-type-if-check + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + } + } + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find native/banner/video impression \"%s\" ", impID), + } +} + +func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { + for _, imp := range imps { + if imp.ID == impID { + // ruleid: bid-type-if-check + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner + } + } + } + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find native/banner/video impression \"%s\" ", impID), + } +} diff --git a/.semgrep/adapter/bid-type-if-check.yml b/.semgrep/adapter/bid-type-if-check.yml new file mode 100644 index 00000000000..a2aa238e5f7 --- /dev/null +++ b/.semgrep/adapter/bid-type-if-check.yml @@ -0,0 +1,19 @@ +rules: + - id: bid-type-if-check + message: The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to $ORTBTYPE. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the [MType](https://github.com/prebid/openrtb/blob/main/openrtb2/bid.go#L334) field in the response to accurately determine the media type for the impression. + languages: + - go + severity: WARNING + patterns: + - pattern-inside: | + if $CONDITION != nil { + return $ORTBTYPE + } + - metavariable-pattern: + metavariable: $ORTBTYPE + patterns: + - pattern-either: + - pattern: openrtb_ext.$BIDTYPE + - metavariable-regex: + metavariable: $BIDTYPE + regex: BidType* \ No newline at end of file diff --git a/.semgrep/adapter/bid-type-switch-check.go b/.semgrep/adapter/bid-type-switch-check.go new file mode 100644 index 00000000000..c8adf4f9834 --- /dev/null +++ b/.semgrep/adapter/bid-type-switch-check.go @@ -0,0 +1,36 @@ +/* + bid-type-switch-check tests + https://semgrep.dev/docs/writing-rules/testing-rules + "ruleid" prefix in comment indicates patterns that should be flagged by semgrep + "ok" prefix in comment indidcates patterns that should not be flagged by the semgrep +*/ + +// ruleid: bid-type-switch-check +switch bidExt.AdCodeType { +case "banner": + return openrtb_ext.BidTypeBanner, nil +case "native": + return openrtb_ext.BidTypeNative, nil +case "video": + return openrtb_ext.BidTypeVideo, nil +} + +// ruleid: bid-type-switch-check +switch impExt.Adot.MediaType { +case string(openrtb_ext.BidTypeBanner): + return openrtb_ext.BidTypeBanner, nil +case string(openrtb_ext.BidTypeVideo): + return openrtb_ext.BidTypeVideo, nil +case string(openrtb_ext.BidTypeNative): + return openrtb_ext.BidTypeNative, nil +} + +// ok: bid-type-switch-check +switch bid.MType { +case "banner": + return openrtb_ext.BidTypeBanner, nil +case "native": + return openrtb_ext.BidTypeNative, nil +case "video": + return openrtb_ext.BidTypeVideo, nil +} diff --git a/.semgrep/adapter/bid-type-switch-check.yml b/.semgrep/adapter/bid-type-switch-check.yml new file mode 100644 index 00000000000..b39cf870aa7 --- /dev/null +++ b/.semgrep/adapter/bid-type-switch-check.yml @@ -0,0 +1,23 @@ +rules: + - id: bid-type-switch-check + message: The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to $ORTBTYPE. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the [MType](https://github.com/prebid/openrtb/blob/main/openrtb2/bid.go#L334) field in the response to accurately determine the media type for the impression. + languages: + - go + severity: WARNING + patterns: + - pattern-inside: | + switch $BIDTYPE { + case ...: + return $ORTBTYPE, nil + } + - metavariable-regex: + metavariable: $BIDTYPE + regex: ^(?!bid\.MType$).*$ + - metavariable-pattern: + metavariable: $ORTBTYPE + patterns: + - pattern-either: + - pattern: openrtb_ext.$W + - metavariable-regex: + metavariable: $W + regex: BidType* \ No newline at end of file diff --git a/.semgrep/adapter/builder-struct-name.go b/.semgrep/adapter/builder-struct-name.go new file mode 100644 index 00000000000..37a938ca748 --- /dev/null +++ b/.semgrep/adapter/builder-struct-name.go @@ -0,0 +1,124 @@ +/* + builder-struct-name tests + https://semgrep.dev/docs/writing-rules/testing-rules + "ruleid" prefix in comment indicates patterns that should be flagged by semgrep + "ok" prefix in comment indidcates patterns that should not be flagged by the semgrep +*/ + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + foo1 := foo{} + // ruleid: builder-struct-name-check + return &fooadapter{foo: foo1}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + // ruleid: builder-struct-name-check + return &adapterbar{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + // ruleid: builder-struct-name-check + return &fooadapterbar{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + // ruleid: builder-struct-name-check + return &FooAdapter{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + // ruleid: builder-struct-name-check + return &AdapterBar{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + // ruleid: builder-struct-name-check + return &AdapterBar{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + // ruleid: builder-struct-name-check + return &FooAdapterBar{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + foo2 := foo{} + //ruleid: builder-struct-name-check + adpt1 := Adapter{foo: foo2} + return &adpt1, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + //ruleid: builder-struct-name-check + builder := &Adapter{foo{}} + return builder, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + foo3 := foo{} + if foo3.bar == "" { + foo3.bar = "bar" + } + //ruleid: builder-struct-name-check + adpt2 := Adapter{} + adpt2.foo = foo3 + return &adpt2, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + //ruleid: builder-struct-name-check + return &foo{}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + //ruleid: builder-struct-name-check + var obj Adapter + obj.Foo = "foo" + if obj.Bar == "" { + obj.Bar = "bar" + } + return &obj, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + //ruleid: builder-struct-name-check + var obj *FooAdapterBar + obj.Foo = "foo" + if obj.Bar == "" { + obj.Bar = "bar" + } + return obj, nil +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + // ok: builder-struct-name-check + return &adapter{endpoint: "www.foo.com"}, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + builder := &adapter{} + builder.endpoint = "www.foo.com" + // ok: builder-struct-name-check + return builder, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + builder := adapter{} + builder.endpoint = "www.foo.com" + // ok: builder-struct-name-check + return &builder, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + var builder adapter + builder.endpoint = "www.foo.com" + // ok: builder-struct-name-check + return &builder, nil +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + var builder *adapter + builder.endpoint = "www.foo.com" + // ok: builder-struct-name-check + return builder, nil +} \ No newline at end of file diff --git a/.semgrep/adapter/builder-struct-name.yml b/.semgrep/adapter/builder-struct-name.yml new file mode 100644 index 00000000000..bc876ae1809 --- /dev/null +++ b/.semgrep/adapter/builder-struct-name.yml @@ -0,0 +1,58 @@ +rules: + - id: builder-struct-name-check + languages: + - go + message: | + You can call this simply "adapter", the `$BUILDER` identification is already supplied by the package name. As you have it, referencing your adapter from outside the package would be `$BUILDER.$BUILDER` which looks a little redundant. See example below: + + ``` + package foo + + type adapter struct { + endpoint string + } + + func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + return &adapter{endpoint: "https://www.foo.com"}, nil + } + ``` + severity: ERROR + patterns: + - pattern-either: + - pattern-inside: > + func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) { + ... + $BUILDER_OBJ := &$BUILDER{...} + ... + return $BUILDER_OBJ, nil + } + - pattern-inside: > + func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) { + ... + $BUILDER_OBJ := $BUILDER{...} + ... + return &$BUILDER_OBJ, nil + } + - pattern-inside: > + func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) { + ... + return &$BUILDER{...}, ... + } + - pattern-inside: > + func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) { + ... + var $BUILDER_OBJ $BUILDER + ... + return &$BUILDER_OBJ, ... + } + - pattern-inside: > + func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) { + ... + var $BUILDER_OBJ *$BUILDER + ... + return $BUILDER_OBJ, ... + } + - focus-metavariable: $BUILDER + - metavariable-regex: + metavariable: $BUILDER + regex: (?!adapter$) diff --git a/.semgrep/adapter/package-import.go b/.semgrep/adapter/package-import.go new file mode 100644 index 00000000000..a34ce8789cd --- /dev/null +++ b/.semgrep/adapter/package-import.go @@ -0,0 +1,47 @@ +import ( + // ok: package-import-check + "fmt" + // ok: package-import-check + "os" + // ruleid: package-import-check + "github.com/mitchellh/copystructure" + // ruleid: package-import-check + "github.com/golang/glog" +) + +import ( + // ok: package-import-check + "fmt" + // ruleid: package-import-check + cs "github.com/mitchellh/copystructure" + // ok: package-import-check + "os" + // ruleid: package-import-check + log "github.com/golang/glog" +) + +import ( + // ok: package-import-check + "fmt" + // ruleid: package-import-check + cs "github.com/mitchellh/copystructure/subpackage" + // ok: package-import-check + "os" + // ruleid: package-import-check + log "github.com/golang/glog/subpackage" +) + +// ruleid: package-import-check +import "github.com/golang/glog" + +// ruleid: package-import-check +import "github.com/mitchellh/copystructure" + +// ruleid: package-import-check +import log "github.com/golang/glog" + +// ruleid: package-import-check +import copy "github.com/mitchellh/copystructure" + +// ok: package-import-check +import "fmt" diff --git a/.semgrep/adapter/package-import.yml b/.semgrep/adapter/package-import.yml new file mode 100644 index 00000000000..20de2d107da --- /dev/null +++ b/.semgrep/adapter/package-import.yml @@ -0,0 +1,13 @@ +rules: + - id: package-import-check + message: Usage of "$PKG" package is prohibited in adapter code + languages: + - go + severity: ERROR + pattern-either: + - patterns: + - pattern: import "$PKG" + - focus-metavariable: $PKG + - metavariable-regex: + metavariable: $PKG + regex: (^github\.com/mitchellh/copystructure(/.*)?$|^github\.com/golang/glog(/.*)?$) \ No newline at end of file diff --git a/.semgrep/adapter/parse-bid-type-check.go b/.semgrep/adapter/parse-bid-type-check.go new file mode 100644 index 00000000000..77badd14b16 --- /dev/null +++ b/.semgrep/adapter/parse-bid-type-check.go @@ -0,0 +1,29 @@ +/* + parse-bid-type-check tests + https://semgrep.dev/docs/writing-rules/testing-rules + "ruleid" prefix in comment indicates patterns that should be flagged by semgrep + "ok" prefix in comment indidcates patterns that should not be flagged by the semgrep +*/ + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + // ruleid: parse-bid-type-check + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse impression \"%s\" mediatype", bid.ImpID), + } +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + var bidExt bidExt + // ruleid: parse-bid-type-check + bidType, err := openrtb_ext.ParseBidType(bidExt.Prebid.Type) + + return bidType, err +} diff --git a/.semgrep/adapter/parse-bid-type-check.yml b/.semgrep/adapter/parse-bid-type-check.yml new file mode 100644 index 00000000000..daa70684023 --- /dev/null +++ b/.semgrep/adapter/parse-bid-type-check.yml @@ -0,0 +1,10 @@ +rules: + - id: parse-bid-type-check + message: > + Prebid server expects the media type to be explicitly set in the adapter response. + Therefore, recommends implementing a pattern where the adapter server sets the [MType](https://github.com/prebid/openrtb/blob/main/openrtb2/bid.go#L334) field in the response to accurately determine the media type for the impression. + languages: + - go + severity: WARNING + patterns: + - pattern: openrtb_ext.ParseBidType(...) \ No newline at end of file diff --git a/.semgrep/adapter/type-bid-assignment.go b/.semgrep/adapter/type-bid-assignment.go new file mode 100644 index 00000000000..fb29a4912d5 --- /dev/null +++ b/.semgrep/adapter/type-bid-assignment.go @@ -0,0 +1,181 @@ +/* + type-bid-assignment tests + https://semgrep.dev/docs/writing-rules/testing-rules + "ruleid" prefix in comment indicates patterns that should be flagged by semgrep + "ok" prefix in comment indidcates patterns that should not be flagged by the semgrep +*/ + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + for _, seatBid := range bidResp.SeatBid { + for _, sb := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + // ruleid: type-bid-assignment-check + Bid: &sb, + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + for _, seatBid := range bidResp.SeatBid { + for _, sb := range seatBid.Bid { + sbcopy := sb + bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + // ok: type-bid-assignment-check + Bid: &sbcopy, + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + for _, seatBid := range bidResp.SeatBid { + for _, sb := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp) + if err != nil { + return nil, err + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + // ruleid: type-bid-assignment-check + Bid: &sb, + BidType: bidType, + }) + + } + } +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + for _, seatBid := range bidResp.SeatBid { + for _, sb := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp) + if err != nil { + return nil, err + } + // ruleid: type-bid-assignment-check + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &sb, BidType: bidType}) + } + } +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + var errors []error + for _, seatBid := range bidResp.SeatBid { + for _, bid := range seatBid.Bid { + var t adapters.TypedBid + // ruleid: type-bid-assignment-check + t.Bid = &bid + bidResponse.Bids = append(bidResponse.Bids, &t) + } + } + return bidResponse, errors +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + var errors []error + for _, seatBid := range bidResp.SeatBid { + for _, bid := range seatBid.Bid { + var t adapters.TypedBid + t = adapters.TypedBid{ + // ruleid: type-bid-assignment-check + Bid: &bid, + } + + bidResponse.Bids = append(bidResponse.Bids, &t) + } + } + return bidResponse, errors +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + for _, seatBid := range bidResp.SeatBid { + for idx, _ := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + // ok: type-bid-assignment-check + Bid: &seatBid.Bid[idx], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + for _, seatBid := range bidResp.SeatBid { + for idx := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp) + if err != nil { + return nil, err + } + // ok: type-bid-assignment-check + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &seatBid.Bid[idx], BidType: bidType}) + } + } +} diff --git a/.semgrep/adapter/type-bid-assignment.yml b/.semgrep/adapter/type-bid-assignment.yml new file mode 100644 index 00000000000..95fb6811227 --- /dev/null +++ b/.semgrep/adapter/type-bid-assignment.yml @@ -0,0 +1,64 @@ +rules: + - id: type-bid-assignment-check + languages: + - go + message: > + Found incorrect assignment made to $KEY. $BID variable receives a new value in each iteration of range loop. Assigning the address of $BID `(&$BID)` to $KEY will result in a pointer that always points to the same memory address with the value of the last iteration. + This can lead to unexpected behavior or incorrect results. Refer https://go.dev/play/p/9ZS1f-5h4qS + + Consider using an index variable in the seatBids.Bid loop as shown below + + ``` + for _, seatBid := range response.SeatBid { + for i := range seatBids.Bid { + ... + responseBid := &adapters.TypedBid{ + Bid: &seatBids.Bid[i], + ... + } + ... + ... + } + } + ``` + severity: ERROR + patterns: + - pattern-either: + - pattern: > + for _, $BID := range ... { + ... + ... := &adapters.TypedBid{ + $KEY: &$BID, + ... + } + ... + } + - pattern: > + for _, $BID := range ... { + ... + ... = adapters.TypedBid{ + $KEY: &$BID, + ... + } + ... + } + - pattern: > + for _, $BID := range ... { + ... + ... = append(..., &adapters.TypedBid{ + $KEY: &$BID, + ... + }) + ... + } + - pattern: > + for _, $BID := range ... { + var $TYPEBID_OBJ adapters.TypedBid + ... + $TYPEBID_OBJ.$KEY = &$BID + ... + } + - focus-metavariable: $KEY + - metavariable-regex: + metavariable: $KEY + regex: Bid diff --git a/Dockerfile b/Dockerfile index 80269c908df..090d9a79954 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ RUN apt-get update && \ apt-get -y upgrade && \ apt-get install -y wget WORKDIR /tmp -RUN wget https://dl.google.com/go/go1.19.2.linux-amd64.tar.gz && \ - tar -xf go1.19.2.linux-amd64.tar.gz && \ +RUN wget https://dl.google.com/go/go1.20.5.linux-amd64.tar.gz && \ + tar -xf go1.20.5.linux-amd64.tar.gz && \ mv go /usr/local RUN mkdir -p /app/prebid-server/ WORKDIR /app/prebid-server/ diff --git a/Makefile b/Makefile index baf69cafbf8..b5b7281e5ed 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ all: deps test build-modules build -.PHONY: deps test build-modules build image +.PHONY: deps test build-modules build image format # deps will clean out the vendor directory and use go mod for a fresh install deps: @@ -29,3 +29,7 @@ build: test # image will build a docker image image: docker build -t prebid-server . + +# format runs format +format: + ./scripts/format.sh -f true \ No newline at end of file diff --git a/README.md b/README.md index 4ec6792c0c8..cb64ed9a3b1 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,23 @@ # Prebid Server Prebid Server is an open source implementation of Server-Side Header Bidding. -It is managed by [Prebid.org](http://prebid.org/overview/what-is-prebid-org.html), -and upholds the principles from the [Prebid Code of Conduct](http://prebid.org/wrapper_code_of_conduct.html). +It is managed by [Prebid.org](https://prebid.org/about/), +and upholds the principles from the [Prebid Code of Conduct](https://prebid.org/code-of-conduct/). This project does not support the same set of Bidders as Prebid.js, although there is overlap. The current set can be found in the [adapters](./adapters) package. If you don't see the one you want, feel free to [contribute it](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html). For more information, see: -- [What is Prebid?](https://prebid.org/overview/intro.html) +- [What is Prebid?](https://docs.prebid.org/overview/intro.html) - [Prebid Server Overview](https://docs.prebid.org/prebid-server/overview/prebid-server-overview.html) -- [Current Bidders](http://prebid.org/dev-docs/pbs-bidders.html) +- [Current Bidders](https://docs.prebid.org/dev-docs/pbs-bidders.html) Please consider [registering your Prebid Server](https://docs.prebid.org/prebid-server/hosting/pbs-hosting.html#optional-registration) to get on the mailing list for updates, etc. ## Installation -First install [Go](https://golang.org/doc/install) version 1.18 or newer. +First install [Go](https://golang.org/doc/install) version 1.19 or newer. Note that prebid-server is using [Go modules](https://blog.golang.org/using-go-modules). We officially support the most recent two major versions of the Go runtime. However, if you'd like to use a version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`. @@ -47,6 +47,15 @@ go build . ./prebid-server ``` +Run format: +``` +make format +``` +or +```bash +./scripts/format.sh -f true +``` + Load the landing page in your browser at `http://localhost:8000/`. For the full API reference, see [the endpoint documentation](https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-overview.html) diff --git a/account/account.go b/account/account.go index a32423d1367..156ad5e6d39 100644 --- a/account/account.go +++ b/account/account.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/buger/jsonparser" "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/config" @@ -12,6 +11,7 @@ import ( "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/stored_requests" + "github.com/prebid/prebid-server/util/iputil" jsonpatch "gopkg.in/evanphx/json-patch.v4" ) @@ -102,6 +102,18 @@ func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_r }) return nil, errs } + + if ipV6Err := account.Privacy.IPv6Config.Validate(nil); len(ipV6Err) > 0 { + account.Privacy.IPv6Config.AnonKeepBits = iputil.IPv6DefaultMaskingBitSize + } + + if ipV4Err := account.Privacy.IPv4Config.Validate(nil); len(ipV4Err) > 0 { + account.Privacy.IPv4Config.AnonKeepBits = iputil.IPv4DefaultMaskingBitSize + } + + // set the value of events.enabled field based on deprecated events_enabled field and ensure backward compatibility + deprecateEventsEnabledField(account) + return account, nil } @@ -252,3 +264,16 @@ func useGDPRChannelEnabled(account *config.Account) bool { func useCCPAChannelEnabled(account *config.Account) bool { return account.CCPA.ChannelEnabled.IsSet() && !account.CCPA.IntegrationEnabled.IsSet() } + +// deprecateEventsEnabledField is responsible for ensuring backwards compatibility of "events_enabled" field. +// This function favors "events.enabled" field over deprecated "events_enabled" field, if values for both are set. +// If only deprecated "events_enabled" field is set then it sets the same value to "events.enabled" field. +func deprecateEventsEnabledField(account *config.Account) { + if account != nil { + if account.Events.Enabled == nil { + account.Events.Enabled = account.EventsEnabled + } + // assign the old value to the new value so old and new are always the same even though the new value is what is used in the application code. + account.EventsEnabled = account.Events.Enabled + } +} diff --git a/account/account_test.go b/account/account_test.go index 95fa8ada729..d2694abc5a1 100644 --- a/account/account_test.go +++ b/account/account_test.go @@ -12,16 +12,19 @@ import ( "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/stored_requests" + "github.com/prebid/prebid-server/util/iputil" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) var mockAccountData = map[string]json.RawMessage{ - "valid_acct": json.RawMessage(`{"disabled":false}`), - "disabled_acct": json.RawMessage(`{"disabled":true}`), - "malformed_acct": json.RawMessage(`{"disabled":"invalid type"}`), - "gdpr_channel_enabled_acct": json.RawMessage(`{"disabled":false,"gdpr":{"channel_enabled":{"amp":true}}}`), - "ccpa_channel_enabled_acct": json.RawMessage(`{"disabled":false,"ccpa":{"channel_enabled":{"amp":true}}}`), + "valid_acct": json.RawMessage(`{"disabled":false}`), + "invalid_acct_ipv6_ipv4": json.RawMessage(`{"disabled":false, "privacy": {"ipv6": {"anon_keep_bits": -32}, "ipv4": {"anon_keep_bits": -16}}}`), + "disabled_acct": json.RawMessage(`{"disabled":true}`), + "malformed_acct": json.RawMessage(`{"disabled":"invalid type"}`), + "gdpr_channel_enabled_acct": json.RawMessage(`{"disabled":false,"gdpr":{"channel_enabled":{"amp":true}}}`), + "ccpa_channel_enabled_acct": json.RawMessage(`{"disabled":false,"ccpa":{"channel_enabled":{"amp":true}}}`), "gdpr_channel_enabled_deprecated_purpose_acct": json.RawMessage(`{"disabled":false,"gdpr":{"purpose1":{"enforce_purpose":"full"}, "channel_enabled":{"amp":true}}}`), "gdpr_deprecated_purpose1": json.RawMessage(`{"disabled":false,"gdpr":{"purpose1":{"enforce_purpose":"full"}}}`), "gdpr_deprecated_purpose2": json.RawMessage(`{"disabled":false,"gdpr":{"purpose2":{"enforce_purpose":"full"}}}`), @@ -53,6 +56,8 @@ func TestGetAccount(t *testing.T) { required bool // account_defaults.disabled disabled bool + // checkDefaultIP indicates IPv6 and IPv6 should be set to default values + checkDefaultIP bool // expected error, or nil if account should be found err error }{ @@ -77,6 +82,8 @@ func TestGetAccount(t *testing.T) { {accountID: "valid_acct", required: false, disabled: true, err: nil}, {accountID: "valid_acct", required: true, disabled: true, err: nil}, + {accountID: "invalid_acct_ipv6_ipv4", required: true, disabled: false, err: nil, checkDefaultIP: true}, + // pubID given and matches a host account explicitly disabled (Disabled: true on account json) {accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, {accountID: "disabled_acct", required: true, disabled: false, err: &errortypes.BlacklistedAcct{}}, @@ -128,6 +135,10 @@ func TestGetAccount(t *testing.T) { assert.Nil(t, account, "return account must be nil on error") assert.IsType(t, test.err, errors[0], "error is of unexpected type") } + if test.checkDefaultIP { + assert.Equal(t, account.Privacy.IPv6Config.AnonKeepBits, iputil.IPv6DefaultMaskingBitSize, "ipv6 should be set to default value") + assert.Equal(t, account.Privacy.IPv4Config.AnonKeepBits, iputil.IPv4DefaultMaskingBitSize, "ipv4 should be set to default value") + } }) } } @@ -579,3 +590,66 @@ func TestAccountUpgradeStatusGetAccount(t *testing.T) { }) } } + +func TestDeprecateEventsEnabledField(t *testing.T) { + testCases := []struct { + name string + account *config.Account + want *bool + }{ + { + name: "account is nil", + account: nil, + want: nil, + }, + { + name: "account.EventsEnabled is nil, account.Events.Enabled is nil", + account: &config.Account{ + EventsEnabled: nil, + Events: config.Events{ + Enabled: nil, + }, + }, + want: nil, + }, + { + name: "account.EventsEnabled is nil, account.Events.Enabled is non-nil", + account: &config.Account{ + EventsEnabled: nil, + Events: config.Events{ + Enabled: ptrutil.ToPtr(true), + }, + }, + want: ptrutil.ToPtr(true), + }, + { + name: "account.EventsEnabled is non-nil, account.Events.Enabled is nil", + account: &config.Account{ + EventsEnabled: ptrutil.ToPtr(true), + Events: config.Events{ + Enabled: nil, + }, + }, + want: ptrutil.ToPtr(true), + }, + { + name: "account.EventsEnabled is non-nil, account.Events.Enabled is non-nil", + account: &config.Account{ + EventsEnabled: ptrutil.ToPtr(false), + Events: config.Events{ + Enabled: ptrutil.ToPtr(true), + }, + }, + want: ptrutil.ToPtr(true), + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + deprecateEventsEnabledField(test.account) + if test.account != nil { + assert.Equal(t, test.want, test.account.Events.Enabled) + } + }) + } +} diff --git a/adapters/aax/aaxtest/supplemental/invalid-resp-diff-imp-id.json b/adapters/aax/aaxtest/supplemental/invalid-resp-diff-imp-id.json index c415c274fcf..2d9d02d66ba 100644 --- a/adapters/aax/aaxtest/supplemental/invalid-resp-diff-imp-id.json +++ b/adapters/aax/aaxtest/supplemental/invalid-resp-diff-imp-id.json @@ -115,11 +115,11 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unable to fetch mediaType in multi-format: some-other-imp-id", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/aax/aaxtest/supplemental/invalid-resp-multi-imp-type.json b/adapters/aax/aaxtest/supplemental/invalid-resp-multi-imp-type.json index 23aa0ae6760..b0a0b750ab1 100644 --- a/adapters/aax/aaxtest/supplemental/invalid-resp-multi-imp-type.json +++ b/adapters/aax/aaxtest/supplemental/invalid-resp-multi-imp-type.json @@ -141,11 +141,11 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unable to fetch mediaType in multi-format: test-imp-id", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go index 6748cb8a15d..39b1e945f7d 100644 --- a/adapters/adapterstest/test_json.go +++ b/adapters/adapterstest/test_json.go @@ -3,8 +3,10 @@ package adapterstest import ( "encoding/json" "fmt" + "io/fs" "net/http" "os" + "path/filepath" "regexp" "testing" @@ -18,6 +20,16 @@ import ( "github.com/yudai/gojsondiff/formatter" ) +const jsonFileExtension string = ".json" + +var supportedDirs = map[string]struct{}{ + "exemplary": {}, + "supplemental": {}, + "amp": {}, + "video": {}, + "videosupplemental": {}, +} + // RunJSONBidderTest is a helper method intended to unit test Bidders' adapters. // It requires that: // @@ -53,29 +65,36 @@ import ( // adapterstest.RunJSONBidderTest(t, "{bidder}test", instanceOfYourBidder) // } func RunJSONBidderTest(t *testing.T, rootDir string, bidder adapters.Bidder) { - runTests(t, fmt.Sprintf("%s/exemplary", rootDir), bidder, false, false, false) - runTests(t, fmt.Sprintf("%s/supplemental", rootDir), bidder, true, false, false) - runTests(t, fmt.Sprintf("%s/amp", rootDir), bidder, true, true, false) - runTests(t, fmt.Sprintf("%s/video", rootDir), bidder, false, false, true) - runTests(t, fmt.Sprintf("%s/videosupplemental", rootDir), bidder, true, false, true) + err := filepath.WalkDir(rootDir, func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + + isJsonFile := !info.IsDir() && filepath.Ext(info.Name()) == jsonFileExtension + RunSingleJSONBidderTest(t, bidder, path, isJsonFile) + return nil + }) + assert.NoError(t, err, "Error reading files from directory %s \n", rootDir) } -// runTests runs all the *.json files in a directory. If allowErrors is false, and one of the test files -// expects errors from the bidder, then the test will fail. -func runTests(t *testing.T, directory string, bidder adapters.Bidder, allowErrors, isAmpTest, isVideoTest bool) { - if specFiles, err := os.ReadDir(directory); err == nil { - for _, specFile := range specFiles { - fileName := fmt.Sprintf("%s/%s", directory, specFile.Name()) - specData, err := loadFile(fileName) - if err != nil { - t.Fatalf("Failed to load contents of file %s: %v", fileName, err) - } +func RunSingleJSONBidderTest(t *testing.T, bidder adapters.Bidder, path string, isJsonFile bool) { + base := filepath.Base(filepath.Dir(path)) + if _, ok := supportedDirs[base]; !ok { + return + } - if !allowErrors && specData.expectsErrors() { - t.Fatalf("Exemplary spec %s must not expect errors.", fileName) - } - runSpec(t, fileName, specData, bidder, isAmpTest, isVideoTest) + allowErrors := base != "exemplary" && base != "video" + if isJsonFile { + specData, err := loadFile(path) + if err != nil { + t.Fatalf("Failed to load contents of file %s: %v", path, err) } + + if !allowErrors && specData.expectsErrors() { + t.Fatalf("Exemplary spec %s must not expect errors.", path) + } + + runSpec(t, path, specData, bidder, base == "amp", base == "videosupplemental" || base == "video") } } @@ -258,22 +277,8 @@ func assertErrorList(t *testing.T, description string, actual []error, expected func assertMakeBidsOutput(t *testing.T, filename string, bidderResponse *adapters.BidderResponse, expected expectedBidResponse) { t.Helper() - - if (bidderResponse == nil || len(bidderResponse.Bids) == 0) != (len(expected.Bids) == 0) { - if len(expected.Bids) == 0 { - t.Fatalf("%s: expectedBidResponses indicated a nil response, but mockResponses supplied a non-nil response", filename) - } - - t.Fatalf("%s: mockResponses included unexpected nil or empty response", filename) - } - - // Expected nil response - give diffBids something to work with. - if bidderResponse == nil { - bidderResponse = new(adapters.BidderResponse) - } - - if len(bidderResponse.Bids) != len(expected.Bids) { - t.Fatalf("%s: MakeBids returned wrong bid count. Expected %d, got %d", filename, len(expected.Bids), len(bidderResponse.Bids)) + if !assert.Len(t, bidderResponse.Bids, len(expected.Bids), "%s: Wrong MakeBids bidderResponse.Bids count. len(bidderResponse.Bids) = %d vs len(spec.BidResponses.Bids) = %d", filename, len(bidderResponse.Bids), len(expected.Bids)) { + return } for i := 0; i < len(bidderResponse.Bids); i++ { diffBids(t, fmt.Sprintf("%s: typedBid[%d]", filename, i), bidderResponse.Bids[i], &(expected.Bids[i])) @@ -435,15 +440,21 @@ func testMakeBidsImpl(t *testing.T, filename string, spec *testSpec, bidder adap // output inside testMakeRequestsImpl thisBidResponse, theseErrs := bidder.MakeBids(&spec.BidRequest, spec.HttpCalls[i].Request.ToRequestData(t), spec.HttpCalls[i].Response.ToResponseData(t)) - bidsErrs = append(bidsErrs, theseErrs...) - bidResponses = append(bidResponses, thisBidResponse) + if theseErrs != nil { + bidsErrs = append(bidsErrs, theseErrs...) + } + if thisBidResponse != nil { + bidResponses = append(bidResponses, thisBidResponse) + } } // Assert actual errors thrown by MakeBids implementation versus expected JSON-defined spec.MakeBidsErrors assertErrorList(t, fmt.Sprintf("%s: MakeBids", filename), bidsErrs, spec.MakeBidsErrors) // Assert MakeBids implementation BidResponses with expected JSON-defined spec.BidResponses[i].Bids - for i := 0; i < len(spec.BidResponses); i++ { - assertMakeBidsOutput(t, filename, bidResponses[i], spec.BidResponses[i]) + if assert.Len(t, bidResponses, len(spec.BidResponses), "%s: MakeBids len(bidResponses) = %d vs len(spec.BidResponses) = %d", filename, len(bidResponses), len(spec.BidResponses)) { + for i := 0; i < len(spec.BidResponses); i++ { + assertMakeBidsOutput(t, filename, bidResponses[i], spec.BidResponses[i]) + } } } diff --git a/adapters/adf/adftest/supplemental/invalid-imp-mediatype.json b/adapters/adf/adftest/supplemental/invalid-imp-mediatype.json index 8a87492a2dc..5dab8c36fa2 100644 --- a/adapters/adf/adftest/supplemental/invalid-imp-mediatype.json +++ b/adapters/adf/adftest/supplemental/invalid-imp-mediatype.json @@ -79,7 +79,7 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to parse impression \"test-imp-id\" mediatype", diff --git a/adapters/adf/adftest/supplemental/nobid-response.json b/adapters/adf/adftest/supplemental/nobid-response.json index ec559aa7468..57dd2d5c626 100644 --- a/adapters/adf/adftest/supplemental/nobid-response.json +++ b/adapters/adf/adftest/supplemental/nobid-response.json @@ -44,6 +44,6 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [] } diff --git a/adapters/adhese/adhese.go b/adapters/adhese/adhese.go index 429dab2301d..9c84676c379 100644 --- a/adapters/adhese/adhese.go +++ b/adapters/adhese/adhese.go @@ -10,7 +10,6 @@ import ( "strings" "text/template" - "github.com/golang/glog" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" @@ -201,7 +200,6 @@ func (a *AdheseAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR func convertAdheseBid(adheseBid AdheseBid, adheseExt AdheseExt, adheseOriginData AdheseOriginData) openrtb2.BidResponse { adheseExtJson, err := json.Marshal(adheseOriginData) if err != nil { - glog.Error(fmt.Sprintf("Unable to parse adhese Origin Data as JSON due to %v", err)) adheseExtJson = make([]byte, 0) } return openrtb2.BidResponse{ diff --git a/adapters/adkernel/adkernel.go b/adapters/adkernel/adkernel.go index 478b2ad3e2f..5fae001d7dd 100644 --- a/adapters/adkernel/adkernel.go +++ b/adapters/adkernel/adkernel.go @@ -220,6 +220,7 @@ func (adapter *adkernelAdapter) MakeBids(internalRequest *openrtb2.BidRequest, e newBadServerResponseError(fmt.Sprintf("Bad server response: %d", err)), } } + if len(bidResp.SeatBid) != 1 { return nil, []error{ newBadServerResponseError(fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid))), @@ -228,7 +229,7 @@ func (adapter *adkernelAdapter) MakeBids(internalRequest *openrtb2.BidRequest, e seatBid := bidResp.SeatBid[0] bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) - + bidResponse.Currency = bidResp.Cur for i := 0; i < len(seatBid.Bid); i++ { bid := seatBid.Bid[i] bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ diff --git a/adapters/adkernel/adkerneltest/exemplary/single-banner-impression.json b/adapters/adkernel/adkerneltest/exemplary/single-banner-impression.json index 990903e15f6..24f86378fe5 100644 --- a/adapters/adkernel/adkerneltest/exemplary/single-banner-impression.json +++ b/adapters/adkernel/adkerneltest/exemplary/single-banner-impression.json @@ -23,7 +23,8 @@ }, "user": { "buyeruid": "A-38327932832" - } + }, + "cur": ["TYR"] }, "httpCalls": [ @@ -45,7 +46,8 @@ }, "user": { "buyeruid": "A-38327932832" - } + }, + "cur": ["TYR"] } }, "mockResponse": { @@ -67,7 +69,8 @@ "w": 300 }] }], - "bidid": "wehM-93KGr0" + "bidid": "wehM-93KGr0", + "cur": "TYR" } } } @@ -75,7 +78,7 @@ "expectedBidResponses": [ { - "currency": "USD", + "currency": "TYR", "bids": [ { "bid": { diff --git a/adapters/adnuntius/adnuntius.go b/adapters/adnuntius/adnuntius.go index 21830deb9c3..3f91d7403f0 100644 --- a/adapters/adnuntius/adnuntius.go +++ b/adapters/adnuntius/adnuntius.go @@ -27,31 +27,35 @@ type adnAdunit struct { AuId string `json:"auId"` TargetId string `json:"targetId"` Dimensions [][]int64 `json:"dimensions,omitempty"` + MaxDeals int `json:"maxDeals,omitempty"` } type extDeviceAdnuntius struct { NoCookies bool `json:"noCookies,omitempty"` } +type Ad struct { + Bid struct { + Amount float64 + Currency string + } + DealID string `json:"dealId,omitempty"` + AdId string + CreativeWidth string + CreativeHeight string + CreativeId string + LineItemId string + Html string + DestinationUrls map[string]string +} + type AdUnit struct { AuId string TargetId string Html string ResponseId string - Ads []struct { - Bid struct { - Amount float64 - Currency string - } - DealID string `json:"dealId,omitempty"` - AdId string - CreativeWidth string - CreativeHeight string - CreativeId string - LineItemId string - Html string - DestinationUrls map[string]string - } + Ads []Ad + Deals []Ad `json:"deals,omitempty"` } type AdnResponse struct { @@ -213,13 +217,17 @@ func (a *adapter) generateRequests(ortbRequest openrtb2.BidRequest) ([]*adapters network = adnuntiusExt.Network } + adUnit := adnAdunit{ + AuId: adnuntiusExt.Auid, + TargetId: fmt.Sprintf("%s-%s", adnuntiusExt.Auid, imp.ID), + Dimensions: getImpSizes(imp), + } + if adnuntiusExt.MaxDeals > 0 { + adUnit.MaxDeals = adnuntiusExt.MaxDeals + } networkAdunitMap[network] = append( networkAdunitMap[network], - adnAdunit{ - AuId: adnuntiusExt.Auid, - TargetId: fmt.Sprintf("%s-%s", adnuntiusExt.Auid, imp.ID), - Dimensions: getImpSizes(imp), - }) + adUnit) } endpoint, err := makeEndpointUrl(ortbRequest, a, noCookies) @@ -320,6 +328,46 @@ func getGDPR(request *openrtb2.BidRequest) (string, string, error) { return gdpr, consent, nil } +func generateAdResponse(ad Ad, impId string, html string, request *openrtb2.BidRequest) (*openrtb2.Bid, []error) { + + creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64) + if widthErr != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Value of width: %s is not a string", ad.CreativeWidth), + }} + } + + creativeHeight, heightErr := strconv.ParseInt(ad.CreativeHeight, 10, 64) + if heightErr != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Value of height: %s is not a string", ad.CreativeHeight), + }} + } + + adDomain := []string{} + for _, url := range ad.DestinationUrls { + domainArray := strings.Split(url, "/") + domain := strings.Replace(domainArray[2], "www.", "", -1) + adDomain = append(adDomain, domain) + } + + bid := openrtb2.Bid{ + ID: ad.AdId, + ImpID: impId, + W: creativeWidth, + H: creativeHeight, + AdID: ad.AdId, + DealID: ad.DealID, + CID: ad.LineItemId, + CrID: ad.CreativeId, + Price: ad.Bid.Amount * 1000, + AdM: html, + ADomain: adDomain, + } + return &bid, nil + +} + func generateBidResponse(adnResponse *AdnResponse, request *openrtb2.BidRequest) (*adapters.BidderResponse, []error) { bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(adnResponse.AdUnits)) var currency string @@ -344,48 +392,34 @@ func generateBidResponse(adnResponse *AdnResponse, request *openrtb2.BidRequest) if len(adunit.Ads) > 0 { ad := adunit.Ads[0] - currency = ad.Bid.Currency - creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64) - if widthErr != nil { + adBid, err := generateAdResponse(ad, request.Imp[i].ID, adunit.Html, request) + if err != nil { return nil, []error{&errortypes.BadInput{ - Message: fmt.Sprintf("Value of width: %s is not a string", ad.CreativeWidth), + Message: fmt.Sprintf("Error at ad generation"), }} } - creativeHeight, heightErr := strconv.ParseInt(ad.CreativeHeight, 10, 64) - if heightErr != nil { - return nil, []error{&errortypes.BadInput{ - Message: fmt.Sprintf("Value of height: %s is not a string", ad.CreativeHeight), - }} - } - - adDomain := []string{} - for _, url := range ad.DestinationUrls { - domainArray := strings.Split(url, "/") - domain := strings.Replace(domainArray[2], "www.", "", -1) - adDomain = append(adDomain, domain) - } - - bid := openrtb2.Bid{ - ID: ad.AdId, - ImpID: request.Imp[i].ID, - W: creativeWidth, - H: creativeHeight, - AdID: ad.AdId, - DealID: ad.DealID, - CID: ad.LineItemId, - CrID: ad.CreativeId, - Price: ad.Bid.Amount * 1000, - AdM: adunit.Html, - ADomain: adDomain, - } - bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &bid, + Bid: adBid, BidType: "banner", }) + + for _, deal := range adunit.Deals { + dealBid, err := generateAdResponse(deal, request.Imp[i].ID, deal.Html, request) + if err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Error at ad generation"), + }} + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: dealBid, + BidType: "banner", + }) + } + } } diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json b/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json index 035d16566ac..d6f292d8cd5 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json @@ -75,7 +75,7 @@ "adUnits": [ { "auId": "0000000000000456", - "targetId": "456-test-imp-id2", + "targetId": "456-test-imp-id-2", "html": "", "responseId": "adn-rsp-900646517", "ads": [ @@ -122,7 +122,7 @@ } } ], - "expectedBidResponse": + "expectedBidResponses": [ { "bids": [ { @@ -159,4 +159,5 @@ "currency": "NOK" } + ] } diff --git a/adapters/adnuntius/adnuntiustest/supplemental/height-error.json b/adapters/adnuntius/adnuntiustest/supplemental/height-error.json index 1f213a43b6a..1987fb9d08e 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/height-error.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/height-error.json @@ -81,7 +81,7 @@ ], "expectedMakeBidsErrors": [ { - "value": "json: cannot unmarshal number into Go struct field .AdUnits.Ads.CreativeHeight of type string", + "value": "json: cannot unmarshal number into Go struct field Ad.AdUnits.Ads.CreativeHeight of type string", "comparison": "literal" } ] diff --git a/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json b/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json new file mode 100644 index 00000000000..1d4c5bf0747 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json @@ -0,0 +1,168 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123", + "maxDeals": 2 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=json&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]], + "maxDeals": 2 + } + ], + "context": "prebid.org", + "metaData": {} + } + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "matchedAdCount": 1, + "responseId": "adn-rsp-815135818", + "deals": [ + { + "destinationUrlEsc": "", + "clickUrl": "https://click.url", + "urls": { + "url": "https://delivery.adnuntius.com/c/eKOchNsBJzE.net" + }, + "destinationUrls": { + "url": "http://www.google.com" + }, + "cpm": { + "amount": 1.0, + "currency": "NOK" + }, + "bid": { + "amount": 0.001, + "currency": "NOK" + }, + "grossBid": { + "amount": 0.001, + "currency": "NOK" + }, + "netBid": { + "amount": 0.001, + "currency": "NOK" + }, + "cost": { + "amount": 0.001, + "currency": "NOK" + }, + "dealId": "deal_123", + "adId": "adn-id-116330612", + "creativeWidth": "300", + "creativeHeight": "250", + "creativeId": "dl0knc1lnks9jgvx", + "lineItemId": "6l5w2d29kz3vkprq", + "layoutId": "adnuntius_image_layout_1", + "layoutName": "Image", + "layoutExternalReference": "", + "html": "" + } + ], + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 0.001, + "currency": "NOK" + }, + "adId": "adn-id-1559784095", + "creativeWidth": "300", + "creativeHeight": "250", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ], + "metaData": { + "usi": "5dlpmw0d00btldjdvk1lp8rl", + "sessionId": "e4ada7251c93291a871f8e4319cc8fe5" + }, + "duplicateFilter": "AAAAAwAAAAAAJ33PAAAAAAAhrK4AAAAAACYNPAAAAAA=", + "segments": [], + "keywords": [] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784095", + "impid": "test-imp-id", + "price": 1, + "adm": "", + "adid": "adn-id-1559784095", + "adomain": ["google.com"], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 300, + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "id": "adn-id-116330612", + "impid": "test-imp-id", + "price": 1, + "adm": "", + "adid": "adn-id-116330612", + "adomain": ["google.com"], + "cid": "6l5w2d29kz3vkprq", + "crid": "dl0knc1lnks9jgvx", + "dealid": "deal_123", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ], + "currency": "NOK" + } + ] +} diff --git a/adapters/adnuntius/adnuntiustest/supplemental/width-error.json b/adapters/adnuntius/adnuntiustest/supplemental/width-error.json index 1fd8a83f9dd..4f109942b91 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/width-error.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/width-error.json @@ -81,7 +81,7 @@ ], "expectedMakeBidsErrors": [ { - "value": "json: cannot unmarshal number into Go struct field .AdUnits.Ads.CreativeWidth of type string", + "value": "json: cannot unmarshal number into Go struct field Ad.AdUnits.Ads.CreativeWidth of type string", "comparison": "literal" } ] diff --git a/adapters/adocean/adocean.go b/adapters/adocean/adocean.go index ce0f78e8f00..a4e6223be6d 100644 --- a/adapters/adocean/adocean.go +++ b/adapters/adocean/adocean.go @@ -21,7 +21,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -const adapterVersion = "1.2.0" +const adapterVersion = "1.3.0" const maxUriLength = 8000 const measurementCode = ` %s", respData.AdQLib, respData.Tag), + ADomain: respData.ADomains, + CrID: fmt.Sprintf("%d", respData.CrID), + W: width, + H: height, + }, + BidType: respData.MediaType.Name, + }) + + return bidResponse, nil +} + +func buildRequest(bidReq *openrtb2.BidRequest, imp *openrtb2.Imp, ext *openrtb_ext.ImpExtAdQuery) *BidderRequest { + userId := "" + if bidReq.User != nil { + userId = bidReq.User.ID + } + + return &BidderRequest{ + V: prebidVersion, + PlacementCode: ext.PlacementID, + AuctionId: "", + BidType: ext.Type, + AdUnitCode: imp.TagID, + BidQid: userId, + BidId: fmt.Sprintf("%s%s", bidReq.ID, imp.ID), + Bidder: bidderName, + BidderRequestId: bidReq.ID, + BidRequestsCount: 1, + BidderRequestsCount: 1, + Sizes: getImpSizes(imp), + } +} + +func parseExt(ext json.RawMessage) (*openrtb_ext.ImpExtAdQuery, error) { + var bext adapters.ExtImpBidder + err := json.Unmarshal(ext, &bext) + if err != nil { + return nil, err + } + + var adsExt openrtb_ext.ImpExtAdQuery + err = json.Unmarshal(bext.Bidder, &adsExt) + if err != nil { + return nil, err + } + + // not validating, because it should have been done earlier by the server + return &adsExt, nil +} + +func parseResponseJson(respBody []byte) (*ResponseData, float64, int64, int64, []error) { + var response ResponseAdQuery + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, 0, 0, 0, []error{err} + } + + if response.Data == nil { + return nil, 0, 0, 0, nil + } + + var errs []error + price, err := strconv.ParseFloat(response.Data.CPM, 64) + if err != nil { + errs = append(errs, err) + } + width, err := strconv.ParseInt(response.Data.MediaType.Width, 10, 64) + if err != nil { + errs = append(errs, err) + } + height, err := strconv.ParseInt(response.Data.MediaType.Height, 10, 64) + if err != nil { + errs = append(errs, err) + } + + if response.Data.MediaType.Name != openrtb_ext.BidTypeBanner { + return nil, 0, 0, 0, []error{fmt.Errorf("unsupported MediaType: %s", response.Data.MediaType.Name)} + } + + if len(errs) > 0 { + return nil, 0, 0, 0, errs + } + return response.Data, price, width, height, nil +} + +func getImpSizes(imp *openrtb2.Imp) string { + if imp.Banner == nil { + return "" + } + + if len(imp.Banner.Format) > 0 { + sizes := make([]string, len(imp.Banner.Format)) + for i, format := range imp.Banner.Format { + sizes[i] = strconv.FormatInt(format.W, 10) + "x" + strconv.FormatInt(format.H, 10) + } + + return strings.Join(sizes, ",") + } + + if imp.Banner.W != nil && imp.Banner.H != nil { + return strconv.FormatInt(*imp.Banner.W, 10) + "x" + strconv.FormatInt(*imp.Banner.H, 10) + } + + return "" +} diff --git a/adapters/adquery/adquery_test.go b/adapters/adquery/adquery_test.go new file mode 100644 index 00000000000..228d835d6c4 --- /dev/null +++ b/adapters/adquery/adquery_test.go @@ -0,0 +1,20 @@ +package adquery + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "testing" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAdquery, config.Adapter{ + Endpoint: "https://bidder.adquery.io/prebid/bid"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 902, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "adquerytest", bidder) +} diff --git a/adapters/adquery/adquerytest/exemplary/empty.json b/adapters/adquery/adquerytest/exemplary/empty.json new file mode 100644 index 00000000000..04f1d21bcab --- /dev/null +++ b/adapters/adquery/adquerytest/exemplary/empty.json @@ -0,0 +1,12 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "xyz" + }, + "imp": [], + "bidder": "adquery" + }, + "httpCalls": [], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/exemplary/many-imps.json b/adapters/adquery/adquerytest/exemplary/many-imps.json new file mode 100644 index 00000000000..a3985c87dc1 --- /dev/null +++ b/adapters/adquery/adquerytest/exemplary/many-imps.json @@ -0,0 +1,227 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + },{ + "id": "2", + "tagid": "test-banner-imp-id-2", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f898", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc1", + "emission_id": "22e26bd9a702bc1", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "EUR", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + },{ + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id-2", + "bidId": "22e26bd9a702bc2", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f898", + "sizes": "300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc2", + "emission_id": "22e26bd9a702bc2", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "EUR", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "22e26bd9a702bc1", + "impid": "1", + "price": 4.14, + "adm": " ", + "adomain": ["https://s1.adquery.io"], + "crid": "7211", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + }, + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "22e26bd9a702bc2", + "impid": "2", + "price": 4.14, + "adm": " ", + "adomain": ["https://s1.adquery.io"], + "crid": "7211", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/exemplary/no-currency.json b/adapters/adquery/adquerytest/exemplary/no-currency.json new file mode 100644 index 00000000000..e97e4b9beaa --- /dev/null +++ b/adapters/adquery/adquerytest/exemplary/no-currency.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc1", + "emission_id": "22e26bd9a702bc1", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "PLN", + "bids": [ + { + "bid": { + "id": "22e26bd9a702bc1", + "impid": "1", + "price": 4.14, + "adm": " ", + "adomain": ["https://s1.adquery.io"], + "crid": "7211", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/exemplary/ok.json b/adapters/adquery/adquerytest/exemplary/ok.json new file mode 100644 index 00000000000..e725e055293 --- /dev/null +++ b/adapters/adquery/adquerytest/exemplary/ok.json @@ -0,0 +1,124 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc1", + "emission_id": "22e26bd9a702bc1", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "EUR", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "22e26bd9a702bc1", + "impid": "1", + "price": 4.14, + "adm": " ", + "adomain": ["https://s1.adquery.io"], + "crid": "7211", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/exemplary/single-imp-banner-format.json b/adapters/adquery/adquerytest/exemplary/single-imp-banner-format.json new file mode 100644 index 00000000000..7a0092d5f5a --- /dev/null +++ b/adapters/adquery/adquerytest/exemplary/single-imp-banner-format.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "w": 320, + "h": 100 + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc1", + "emission_id": "22e26bd9a702bc1", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "PLN", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "22e26bd9a702bc1", + "impid": "1", + "price": 4.14, + "adm": " ", + "adomain": ["https://s1.adquery.io"], + "crid": "7211", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/data-null.json b/adapters/adquery/adquerytest/supplemental/data-null.json new file mode 100644 index 00000000000..0d21a2bd9f7 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/data-null.json @@ -0,0 +1,72 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": null + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/invalid-numerical-values.json b/adapters/adquery/adquerytest/supplemental/invalid-numerical-values.json new file mode 100644 index 00000000000..daff3d0828c --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/invalid-numerical-values.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc", + "emission_id": "22e26bd9a702bc", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "PLN", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320px", + "height": "50px", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "$4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedMakeBidsErrors": [{ + "value": "strconv.ParseFloat: parsing \"$4.14\": invalid syntax", + "comparison": "literal" + },{ + "value": "strconv.ParseInt: parsing \"320px\": invalid syntax", + "comparison": "literal" + },{ + "value": "strconv.ParseInt: parsing \"50px\": invalid syntax", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/malformed-ext.json b/adapters/adquery/adquerytest/supplemental/malformed-ext.json new file mode 100644 index 00000000000..46aaaed431d --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/malformed-ext.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": [], + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "expectedMakeRequestsErrors": [{ + "value": "json: cannot unmarshal array into Go struct field ImpExtAdQuery.placementId of type string", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/malformed-resp.json b/adapters/adquery/adquerytest/supplemental/malformed-resp.json new file mode 100644 index 00000000000..f7aa271e6fe --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/malformed-resp.json @@ -0,0 +1,108 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc", + "emission_id": "22e26bd9a702bc", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": "string-identifier", + "currency": "PLN", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "banner", + "type": "banner320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedMakeBidsErrors": [{ + "value": "json: cannot unmarshal string into Go struct field ResponseData.data.creationId of type int64", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/mediatype-unknown.json b/adapters/adquery/adquerytest/supplemental/mediatype-unknown.json new file mode 100644 index 00000000000..bf3cdc63f45 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/mediatype-unknown.json @@ -0,0 +1,108 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc", + "emission_id": "22e26bd9a702bc", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "PLN", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "unknown", + "type": "unknown320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedMakeBidsErrors": [{ + "value": "unsupported MediaType: unknown", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/mediatype-video.json b/adapters/adquery/adquerytest/supplemental/mediatype-video.json new file mode 100644 index 00000000000..7becebab291 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/mediatype-video.json @@ -0,0 +1,108 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": { + "requestId": "22e26bd9a702bc", + "emission_id": "22e26bd9a702bc", + "eventTracker": "https://bidder.adquery.io/prebid/ev/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "externalEmissionCodes": "", + "impressionTracker": "https://bidder.adquery.io/prebid/im/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "viewabilityTracker": "https://bidder.adquery.io/prebid/vi/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?ad=1", + "clickTracker": "https://bidder.adquery.io/prebid/cl/d30f79cf7fef47bd7a5611719f936539bec0d2e9/22e26bd9a702bc?url=https%3A%2F%2Fbrodacid.pl%2F%3Futm_source%3Dmobile_open%26utm_medium%3Dcpc%26utm_campaign%3Dhi2023", + "link": "https://brodacid.pl/?utm_source=mobile_open&utm_medium=cpc&utm_campaign=hi2023", + "logo": "https://api.adquery.io/img/adquery.png", + "medias": [ + { + "src": "banner/2023-06-05/17591", + "ext": "zip", + "type": 3 + } + ], + "domain": "https://bidder.adquery.io", + "urlAdq": "https://adquery.io", + "creationId": 7211, + "currency": "PLN", + "adDomains": [ + "https://s1.adquery.io" + ], + "tag": " ", + "adqLib": "https://api.adquery.io/js/adquery-0.1.js?time=10", + "mediaType": { + "width": "320", + "height": "50", + "name": "video", + "type": "video320x50" + }, + "cpm": "4.14", + "qid": "fc08aacb07eac44421ed", + "width": "320", + "height": "50", + "isExpand": false + } + } + } + } + ], + "expectedMakeBidsErrors": [{ + "value": "unsupported MediaType: video", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/no-imp-banner-measures.json b/adapters/adquery/adquerytest/supplemental/no-imp-banner-measures.json new file mode 100644 index 00000000000..953d6de5d8d --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/no-imp-banner-measures.json @@ -0,0 +1,61 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": {}, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": null + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/no-imp-banner.json b/adapters/adquery/adquerytest/supplemental/no-imp-banner.json new file mode 100644 index 00000000000..8589d97f606 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/no-imp-banner.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "data": null + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [] + } + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/resp-bad-request.json b/adapters/adquery/adquerytest/supplemental/resp-bad-request.json new file mode 100644 index 00000000000..cb869625720 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/resp-bad-request.json @@ -0,0 +1,68 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 400, + "headers": {} + } + } + ], + "expectedMakeBidsErrors": [{ + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/resp-no-content.json b/adapters/adquery/adquerytest/supplemental/resp-no-content.json new file mode 100644 index 00000000000..5817e15a533 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/resp-no-content.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 204, + "headers": {} + } + } + ], + "expectedBidResponses": [ + ] +} \ No newline at end of file diff --git a/adapters/adquery/adquerytest/supplemental/resp-server-error.json b/adapters/adquery/adquerytest/supplemental/resp-server-error.json new file mode 100644 index 00000000000..05c1cae8488 --- /dev/null +++ b/adapters/adquery/adquerytest/supplemental/resp-server-error.json @@ -0,0 +1,68 @@ +{ + "mockBidRequest": { + "id": "22e26bd9a702bc", + "user": { + "id": "d93f2a0e5f0fe2cc3a6e" + }, + "imp": [ + { + "id": "1", + "tagid": "test-banner-imp-id", + "bidder": "adquery", + "banner": { + "format": [ + { + "w": 320, + "h": 100 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "type": "banner" + } + } + } + ], + "bidder": "adquery" + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.adquery.io/prebid/bid", + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"], + "X-Openrtb-Version": ["2.5"] + }, + "body": { + "adUnitCode": "test-banner-imp-id", + "bidId": "22e26bd9a702bc1", + "bidQid": "d93f2a0e5f0fe2cc3a6e", + "bidPageUrl": "", + "bidder": "adquery", + "bidderRequestId": "22e26bd9a702bc", + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "placementCode": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", + "sizes": "320x100,300x250", + "type": "banner", + "v": "server" + } + }, + "mockResponse": { + "status": 500, + "headers": {} + } + } + ], + "expectedMakeBidsErrors": [{ + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/adquery/params_test.go b/adapters/adquery/params_test.go new file mode 100644 index 00000000000..cba021007d3 --- /dev/null +++ b/adapters/adquery/params_test.go @@ -0,0 +1,49 @@ +package adquery + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderAdquery, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAdquery, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", "type": "banner300x250"}`, +} + +var invalidParams = []string{ + `{}`, + `{"placementId": 42}`, + `{"type": 3}`, + `{"placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897"}`, + `{"type": "banner"}`, + `{"placementId": 42, "type": "banner"}`, + `{"placementId": "too_short", "type": "banner"}`, + `{"placementId": "6d93f2a0e5f0fe2cc3a6e9e3ade964b43b07f897", "type": ""}`, +} diff --git a/adapters/adquery/types.go b/adapters/adquery/types.go new file mode 100644 index 00000000000..e46afaea63e --- /dev/null +++ b/adapters/adquery/types.go @@ -0,0 +1,42 @@ +package adquery + +import "github.com/prebid/prebid-server/openrtb_ext" + +type BidderRequest struct { + V string `json:"v"` + PlacementCode string `json:"placementCode"` + AuctionId string `json:"auctionId,omitempty"` + BidType string `json:"type"` + AdUnitCode string `json:"adUnitCode"` + BidQid string `json:"bidQid"` + BidId string `json:"bidId"` + Bidder string `json:"bidder"` + BidPageUrl string `json:"bidPageUrl"` + BidderRequestId string `json:"bidderRequestId"` + BidRequestsCount int `json:"bidRequestsCount"` + BidderRequestsCount int `json:"bidderRequestsCount"` + Sizes string `json:"sizes"` +} + +type ResponseAdQuery struct { + Data *ResponseData `json:"data"` +} + +type ResponseData struct { + ReqID string `json:"requestId"` + CrID int64 `json:"creationId"` + Currency string `json:"currency"` + CPM string `json:"cpm"` + Code string `json:"code"` + AdQLib string `json:"adqLib"` + Tag string `json:"tag"` + ADomains []string `json:"adDomains"` + DealID string `json:"dealid"` + MediaType AdQueryMediaType `json:"mediaType"` +} + +type AdQueryMediaType struct { + Name openrtb_ext.BidType `json:"name"` + Width string `json:"width"` + Height string `json:"height"` +} diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json index 0dffdb2bebb..2aac06a9e68 100644 --- a/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json +++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json @@ -68,10 +68,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json b/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json index 2e5aeff311f..894e9fbe04d 100644 --- a/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json +++ b/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json @@ -68,10 +68,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/adview/adviewtest/supplemental/nobid-response.json b/adapters/adview/adviewtest/supplemental/nobid-response.json index 9e9ab678a22..76a96484e12 100644 --- a/adapters/adview/adviewtest/supplemental/nobid-response.json +++ b/adapters/adview/adviewtest/supplemental/nobid-response.json @@ -47,6 +47,6 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [] } diff --git a/adapters/adxcg/adxcg.go b/adapters/adxcg/adxcg.go index 008743914df..6b489d322b0 100644 --- a/adapters/adxcg/adxcg.go +++ b/adapters/adxcg/adxcg.go @@ -84,13 +84,13 @@ func (adapter *adapter) MakeBids( return nil, []error{err} } - bidsCapacity := len(openRTBBidderResponse.SeatBid[0].Bid) - bidderResponse = adapters.NewBidderResponseWithBidsCapacity(bidsCapacity) + bidderResponse = adapters.NewBidderResponseWithBidsCapacity(len(openRTBRequest.Imp)) + bidderResponse.Currency = openRTBBidderResponse.Cur var typedBid *adapters.TypedBid for _, seatBid := range openRTBBidderResponse.SeatBid { for _, bid := range seatBid.Bid { activeBid := bid - bidType, err := getMediaTypeForImp(activeBid.ImpID, openRTBRequest.Imp) + bidType, err := getReturnTypeFromMtypeForImp(activeBid.MType) if err != nil { errs = append(errs, err) continue @@ -105,21 +105,17 @@ func (adapter *adapter) MakeBids( } -func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { - for _, imp := range imps { - if imp.ID == impID { - if imp.Native != nil { - return openrtb_ext.BidTypeNative, nil - } else if imp.Banner != nil { - return openrtb_ext.BidTypeBanner, nil - } else if imp.Video != nil { - return openrtb_ext.BidTypeVideo, nil - } - - } - } - - return "", &errortypes.BadInput{ - Message: fmt.Sprintf("Failed to find native/banner/video impression \"%s\" ", impID), +func getReturnTypeFromMtypeForImp(mType openrtb2.MarkupType) (openrtb_ext.BidType, error) { + switch mType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + default: + return "", &errortypes.BadServerResponse{Message: "Unsupported return type"} } } diff --git a/adapters/adxcg/adxcgtest/exemplary/simple-banner-currency.json b/adapters/adxcg/adxcgtest/exemplary/simple-banner-currency.json new file mode 100644 index 00000000000..8678a62c8a7 --- /dev/null +++ b/adapters/adxcg/adxcgtest/exemplary/simple-banner-currency.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "cur": ["EUR"], + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": {} + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/prebid_server", + "body": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "cur": ["EUR"], + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": {} + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "seat": "adxcg", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300, + "mtype":1 + }] + }], + "cur": "EUR" + } + } + }], + + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/adxcg/adxcgtest/exemplary/simple-banner.json b/adapters/adxcg/adxcgtest/exemplary/simple-banner.json index 85ed18ff1a3..42902014126 100644 --- a/adapters/adxcg/adxcgtest/exemplary/simple-banner.json +++ b/adapters/adxcg/adxcgtest/exemplary/simple-banner.json @@ -55,7 +55,8 @@ "cid": "987", "crid": "12345678", "h": 250, - "w": 300 + "w": 300, + "mtype":1 }] }], "cur": "USD" @@ -75,7 +76,8 @@ "cid": "987", "crid": "12345678", "w": 300, - "h": 250 + "h": 250, + "mtype":1 }, "type": "banner" }] diff --git a/adapters/adxcg/adxcgtest/exemplary/simple-native.json b/adapters/adxcg/adxcgtest/exemplary/simple-native.json index 1a449e601a2..1b0d5b5996c 100644 --- a/adapters/adxcg/adxcgtest/exemplary/simple-native.json +++ b/adapters/adxcg/adxcgtest/exemplary/simple-native.json @@ -42,7 +42,8 @@ "price": 1.16, "adm": "native-ad", "w": 1, - "h": 1 + "h": 1, + "mtype":4 } ] } @@ -63,7 +64,8 @@ "price": 1.16, "adm": "native-ad", "w": 1, - "h": 1 + "h": 1, + "mtype":4 }, "type": "native" } diff --git a/adapters/adxcg/adxcgtest/exemplary/simple-video.json b/adapters/adxcg/adxcgtest/exemplary/simple-video.json index b5f54b9bc44..735f10036b8 100644 --- a/adapters/adxcg/adxcgtest/exemplary/simple-video.json +++ b/adapters/adxcg/adxcgtest/exemplary/simple-video.json @@ -74,7 +74,8 @@ "price": 1.16, "adm": "some-test-ad", "w": 300, - "h": 250 + "h": 250, + "mtype":2 } ] } @@ -95,7 +96,8 @@ "price": 1.16, "adm": "some-test-ad", "w": 300, - "h": 250 + "h": 250, + "mtype":2 }, "type": "video" } diff --git a/adapters/aidem/aidem.go b/adapters/aidem/aidem.go new file mode 100644 index 00000000000..9748f32c957 --- /dev/null +++ b/adapters/aidem/aidem.go @@ -0,0 +1,102 @@ +package aidem + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + reqJson, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.endpoint, + Body: reqJson, + Headers: headers, + }}, nil +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("JSON parsing error: %v", err), + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + + for _, seatBid := range bidResp.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForBid(seatBid.Bid[i]) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs +} + +// Builder builds a new instance of the AIDEM adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + return &adapter{ + endpoint: config.Endpoint, + }, nil +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("Unable to fetch mediaType in multi-format: %s", bid.ImpID) + } +} diff --git a/adapters/aidem/aidem_test.go b/adapters/aidem/aidem_test.go new file mode 100644 index 00000000000..03bcc7e0fb5 --- /dev/null +++ b/adapters/aidem/aidem_test.go @@ -0,0 +1,30 @@ +package aidem + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAidem, config.Adapter{ + Endpoint: "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "aidemtest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderAidem, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Nil(t, buildErr) +} diff --git a/adapters/aidem/aidemtest/exemplary/multi-format.json b/adapters/aidem/aidemtest/exemplary/multi-format.json new file mode 100644 index 00000000000..0c940d4ba59 --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/multi-format.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 320, + "h": 480 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 320, + "h": 480 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 50, + "w": 320, + "mtype": 1 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aidem/aidemtest/exemplary/multi-imps-multi-bid.json b/adapters/aidem/aidemtest/exemplary/multi-imps-multi-bid.json new file mode 100644 index 00000000000..5c2b36948f7 --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/multi-imps-multi-bid.json @@ -0,0 +1,154 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 50, + "w": 320, + "mtype": 1 + }, + { + "id": "test-bid-id-2", + "impid": "2", + "price": 1.10, + "adm": "some-test-ad-2", + "crid": "test-crid-2", + "h": 50, + "w": 300, + "mtype": 1 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + }, + { + "bid": { + "id": "test-bid-id-2", + "impid": "2", + "price": 1.10, + "adm": "some-test-ad-2", + "crid": "test-crid-2", + "w": 300, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aidem/aidemtest/exemplary/multi-imps-single-bid.json b/adapters/aidem/aidemtest/exemplary/multi-imps-single-bid.json new file mode 100644 index 00000000000..9eae7101a90 --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/multi-imps-single-bid.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 50, + "w": 320, + "mtype": 1 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aidem/aidemtest/exemplary/no-bid.json b/adapters/aidem/aidemtest/exemplary/no-bid.json new file mode 100644 index 00000000000..7418425f10b --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/no-bid.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/aidem/aidemtest/exemplary/optional-params.json b/adapters/aidem/aidemtest/exemplary/optional-params.json new file mode 100644 index 00000000000..69511fb595c --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/optional-params.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234", + "placementId": "ext" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234", + "placementId": "ext" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/aidem/aidemtest/exemplary/simple-banner.json b/adapters/aidem/aidemtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..73db297ee42 --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/simple-banner.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 50, + "w": 320, + "mtype": 1 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aidem/aidemtest/exemplary/simple-video.json b/adapters/aidem/aidemtest/exemplary/simple-video.json new file mode 100644 index 00000000000..0daaffaa8cf --- /dev/null +++ b/adapters/aidem/aidemtest/exemplary/simple-video.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 320, + "h": 480 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 320, + "h": 480 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "test-bid-id", + "impid": "1", + "price": 2.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 480, + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "1", + "price": 2.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 480, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/aidem/aidemtest/supplemental/invalid-req-400-status-code-bad-request.json b/adapters/aidem/aidemtest/supplemental/invalid-req-400-status-code-bad-request.json new file mode 100644 index 00000000000..3dea13ef7c9 --- /dev/null +++ b/adapters/aidem/aidemtest/supplemental/invalid-req-400-status-code-bad-request.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "0000-000-000-0000" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "0000-000-000-0000" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/aidem/aidemtest/supplemental/invalid-res-200-status-code-empty-bids.json b/adapters/aidem/aidemtest/supplemental/invalid-res-200-status-code-empty-bids.json new file mode 100644 index 00000000000..dd64125f467 --- /dev/null +++ b/adapters/aidem/aidemtest/supplemental/invalid-res-200-status-code-empty-bids.json @@ -0,0 +1,102 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "0000-000-000-0000" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "0000-000-000-0000" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "tid", + "seatbid": [], + "bidid": "bid01" + } + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/aidem/aidemtest/supplemental/invalid-resp-multi-imp-type.json b/adapters/aidem/aidemtest/supplemental/invalid-resp-multi-imp-type.json new file mode 100644 index 00000000000..95f97d31f72 --- /dev/null +++ b/adapters/aidem/aidemtest/supplemental/invalid-resp-multi-imp-type.json @@ -0,0 +1,151 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "tid", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid01" + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Unable to fetch mediaType in multi-format: test-imp-id", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/aidem/aidemtest/supplemental/valid-req-200-bid-response-from-aidem.json b/adapters/aidem/aidemtest/supplemental/valid-req-200-bid-response-from-aidem.json new file mode 100644 index 00000000000..cbd534cd91c --- /dev/null +++ b/adapters/aidem/aidemtest/supplemental/valid-req-200-bid-response-from-aidem.json @@ -0,0 +1,141 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "tid", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "bidid": "bid01" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/aidem/aidemtest/supplemental/valid-req-204-response-from-aidem.json b/adapters/aidem/aidemtest/supplemental/valid-req-204-response-from-aidem.json new file mode 100644 index 00000000000..ddd4e5a7735 --- /dev/null +++ b/adapters/aidem/aidemtest/supplemental/valid-req-204-response-from-aidem.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fakezero.aidemsrv.com/ortb/v2.6/bid/request", + "body": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "siteId": "TCID", + "publisherId": "1234" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/aidem/params_test.go b/adapters/aidem/params_test.go new file mode 100644 index 00000000000..36190c0bc9f --- /dev/null +++ b/adapters/aidem/params_test.go @@ -0,0 +1,57 @@ +package aidem + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/aidem.json TODO: MUST BE CREATED +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderAidem, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected aidem params: %s", validParam) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAidem, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"siteId":"123", "publisherId":"1234"}`, + `{"siteId":"123", "publisherId":"1234", "placementId":"12345"}`, + `{"siteId":"123", "publisherId":"1234", "rateLimit":1}`, + `{"siteId":"123", "publisherId":"1234", "placementId":"12345", "rateLimit":1}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"siteId":"", "publisherId":""}`, + `{"siteId":"only siteId is present"}`, + `{"publisherId":"only publisherId is present"}`, + `{"ssiteId":"123","ppublisherId":"123"}`, + `{"aid":123, "placementId":"123", "siteId":"321"}`, +} diff --git a/adapters/aja/ajatest/supplemental/invalid-bid-type.json b/adapters/aja/ajatest/supplemental/invalid-bid-type.json index 1bba635f731..52f2c49296a 100644 --- a/adapters/aja/ajatest/supplemental/invalid-bid-type.json +++ b/adapters/aja/ajatest/supplemental/invalid-bid-type.json @@ -60,7 +60,7 @@ } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { @@ -68,4 +68,4 @@ "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/algorix/algorixtest/supplemental/multiformat-no-mediatype-response.json b/adapters/algorix/algorixtest/supplemental/multiformat-no-mediatype-response.json index 3828c52d1d8..4d3ea89454e 100644 --- a/adapters/algorix/algorixtest/supplemental/multiformat-no-mediatype-response.json +++ b/adapters/algorix/algorixtest/supplemental/multiformat-no-mediatype-response.json @@ -81,6 +81,7 @@ } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "comparison": "literal", diff --git a/adapters/apacdex/apacdextest/supplemental/wrong-bid-ext.json b/adapters/apacdex/apacdextest/supplemental/wrong-bid-ext.json index f2842d88d2f..f93cc01171b 100644 --- a/adapters/apacdex/apacdextest/supplemental/wrong-bid-ext.json +++ b/adapters/apacdex/apacdextest/supplemental/wrong-bid-ext.json @@ -66,10 +66,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to parse bid mediatype for impression \"another-imp-id\"", "comparison": "regex" } ] -} \ No newline at end of file +} diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index e56d8f2f33c..ddba6a58de7 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strconv" "strings" @@ -12,7 +13,8 @@ import ( "github.com/prebid/openrtb/v19/adcom1" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/util/httputil" + "github.com/prebid/prebid-server/util/maputil" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/prebid/prebid-server/util/randomutil" "github.com/prebid/prebid-server/adapters" @@ -21,25 +23,26 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -const defaultPlatformID int = 5 +const ( + defaultPlatformID = 5 + maxImpsPerReq = 10 +) type adapter struct { - URI string - iabCategoryMap map[string]string + uri url.URL hbSource int randomGenerator randomutil.RandomGenerator } -var maxImpsPerReq = 10 - // Builder builds a new instance of the AppNexus adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + uri, err := url.Parse(config.Endpoint) + if err != nil { + return nil, err + } + bidder := &adapter{ - URI: config.Endpoint, - iabCategoryMap: map[string]string{ - "1": "IAB20-3", - "9": "IAB5-3", - }, + uri: *uri, hbSource: resolvePlatformID(config.PlatformID), randomGenerator: randomutil.RandomNumberGenerator{}, } @@ -57,16 +60,14 @@ func resolvePlatformID(platformID string) int { } func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - // appnexus adapter expects imp.displaymanagerver to be populated in openrtb2 endpoint // but some SDKs will put it in imp.ext.prebid instead displayManagerVer := buildDisplayManageVer(request) var ( shouldGenerateAdPodId *bool - uniqueMemberIds []string - memberIds = make(map[string]struct{}) - errs = make([]error, 0, len(request.Imp)) + uniqueMemberID string + errs []error ) validImps := []openrtb2.Imp{} @@ -84,9 +85,14 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E memberId := appnexusExt.Member if memberId != "" { - if _, ok := memberIds[memberId]; !ok { - memberIds[memberId] = struct{}{} - uniqueMemberIds = append(uniqueMemberIds, memberId) + // The Appnexus API requires a Member ID in the URL. This means the request may fail if + // different impressions have different member IDs. + // Check for this condition, and log an error if it's a problem. + if uniqueMemberID == "" { + uniqueMemberID = memberId + } else if uniqueMemberID != memberId { + errs = append(errs, fmt.Errorf("all request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained member IDs %s and %s", uniqueMemberID, memberId)) + return nil, errs } } @@ -102,22 +108,16 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E } request.Imp = validImps - requestURI := a.URI - // The Appnexus API requires a Member ID in the URL. This means the request may fail if - // different impressions have different member IDs. - // Check for this condition, and log an error if it's a problem. - if len(uniqueMemberIds) > 0 { - requestURI = appendMemberId(requestURI, uniqueMemberIds[0]) - if len(uniqueMemberIds) > 1 { - errs = append(errs, fmt.Errorf("All request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained: %v", uniqueMemberIds)) - } - } - // If all the requests were malformed, don't bother making a server call with no impressions. if len(request.Imp) == 0 { return nil, errs } + requestURI := a.uri + if uniqueMemberID != "" { + requestURI = appendMemberId(requestURI, uniqueMemberID) + } + // Add Appnexus request level extension var isAMP, isVIDEO int if reqInfo.PbsEntryPoint == metrics.ReqTypeAMP { @@ -126,25 +126,19 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E isVIDEO = 1 } - var reqExt appnexusReqExt - if len(request.Ext) > 0 { - if err := json.Unmarshal(request.Ext, &reqExt); err != nil { - errs = append(errs, err) - return nil, errs - } - } - if reqExt.Appnexus == nil { - reqExt.Appnexus = &appnexusReqExtAppnexus{} + reqExt, err := getRequestExt(request.Ext) + if err != nil { + return nil, append(errs, err) } - includeBrandCategory := reqExt.Prebid.Targeting != nil && reqExt.Prebid.Targeting.IncludeBrandCategory != nil - if includeBrandCategory { - reqExt.Appnexus.BrandCategoryUniqueness = &includeBrandCategory - reqExt.Appnexus.IncludeBrandCategory = &includeBrandCategory + + reqExtAppnexus, err := a.getAppnexusExt(reqExt, isAMP, isVIDEO) + if err != nil { + return nil, append(errs, err) } - reqExt.Appnexus.IsAMP = isAMP - reqExt.Appnexus.HeaderBiddingSource = a.hbSource + isVIDEO - imps := request.Imp + if err := moveSupplyChain(request, reqExt); err != nil { + return nil, append(errs, err) + } // For long form requests if adpodId feature enabled, adpod_id must be sent downstream. // Adpod id is a unique identifier for pod @@ -153,20 +147,20 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E // If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same // If adpodId feature disabled and impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but do not include ad pod id if isVIDEO == 1 && *shouldGenerateAdPodId { - requests, errors := a.buildAdPodRequests(imps, request, reqExt, requestURI) + requests, errors := a.buildAdPodRequests(request.Imp, request, reqExt, reqExtAppnexus, requestURI.String()) return requests, append(errs, errors...) } - requests, errors := splitRequests(imps, request, reqExt, requestURI) + requests, errors := splitRequests(request.Imp, request, reqExt, reqExtAppnexus, requestURI.String()) return requests, append(errs, errors...) } func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { - if httputil.IsResponseStatusCodeNoContent(response) { + if adapters.IsResponseStatusCodeNoContent(response) { return nil, nil } - if err := httputil.CheckResponseStatusCodeForErrors(response); err != nil { + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { return nil, []error{err} } @@ -181,7 +175,7 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest for i := range sb.Bid { bid := sb.Bid[i] - var bidExt appnexusBidExt + var bidExt bidExt if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { errs = append(errs, err) continue @@ -198,7 +192,7 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest bid.Cat = []string{iabCategory} } else if len(bid.Cat) > 1 { //create empty categories array to force bid to be rejected - bid.Cat = make([]string, 0) + bid.Cat = []string{} } bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ @@ -217,6 +211,45 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest return bidderResponse, errs } +func getRequestExt(ext json.RawMessage) (map[string]json.RawMessage, error) { + extMap := make(map[string]json.RawMessage) + + if len(ext) > 0 { + if err := json.Unmarshal(ext, &extMap); err != nil { + return nil, err + } + } + + return extMap, nil +} + +func (a *adapter) getAppnexusExt(extMap map[string]json.RawMessage, isAMP int, isVIDEO int) (bidReqExtAppnexus, error) { + var appnexusExt bidReqExtAppnexus + + if appnexusExtJson, exists := extMap["appnexus"]; exists && len(appnexusExtJson) > 0 { + if err := json.Unmarshal(appnexusExtJson, &appnexusExt); err != nil { + return appnexusExt, err + } + } + + if prebidJson, exists := extMap["prebid"]; exists { + _, valueType, _, err := jsonparser.Get(prebidJson, "targeting", "includebrandcategory") + if err != nil && !errors.Is(err, jsonparser.KeyPathNotFoundError) { + return appnexusExt, err + } + + if valueType == jsonparser.Object { + appnexusExt.BrandCategoryUniqueness = ptrutil.ToPtr(true) + appnexusExt.IncludeBrandCategory = ptrutil.ToPtr(true) + } + } + + appnexusExt.IsAMP = isAMP + appnexusExt.HeaderBiddingSource = a.hbSource + isVIDEO + + return appnexusExt, nil +} + func validateAndBuildAppNexusExt(imp *openrtb2.Imp) (openrtb_ext.ExtImpAppnexus, error) { var bidderExt adapters.ExtImpBidder if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { @@ -262,8 +295,8 @@ func groupByPods(imps []openrtb2.Imp) map[string]([]openrtb2.Imp) { return podImps } -func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExtension appnexusReqExt, uri string) ([]*adapters.RequestData, []error) { - errs := []error{} +func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) { + var errs []error // Initial capacity for future array of requests, memory optimization. // Let's say there are 35 impressions and limit impressions per request equals to 10. // In this case we need to create 4 requests with 10, 10, 10 and 5 impressions. @@ -277,14 +310,20 @@ func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt headers.Add("Content-Type", "application/json;charset=utf-8") headers.Add("Accept", "application/json") - var err error - request.Ext, err = json.Marshal(requestExtension) + appnexusExtJson, err := json.Marshal(requestExtAppnexus) if err != nil { errs = append(errs, err) } - for impsLeft { + requestExtClone := maputil.Clone(requestExt) + requestExtClone["appnexus"] = appnexusExtJson + + request.Ext, err = json.Marshal(requestExtClone) + if err != nil { + errs = append(errs, err) + } + for impsLeft { endInd := startInd + maxImpsPerReq if endInd >= len(imps) { endInd = len(imps) @@ -323,9 +362,11 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus, if appnexusExt.InvCode != "" { imp.TagID = appnexusExt.InvCode } + if imp.BidFloor <= 0 && appnexusExt.Reserve > 0 { imp.BidFloor = appnexusExt.Reserve // This will be broken for non-USD currency. } + if imp.Banner != nil { bannerCopy := *imp.Banner if appnexusExt.Position == "above" { @@ -334,7 +375,6 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus, bannerCopy.Pos = adcom1.PositionBelowFold.Ptr() } - // Fixes #307 if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { firstFormat := bannerCopy.Format[0] bannerCopy.W = &(firstFormat.W) @@ -348,46 +388,29 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus, imp.DisplayManagerVer = displayManagerVer } - impExt := appnexusImpExt{Appnexus: appnexusImpExtAppnexus{ + impExt := impExt{Appnexus: impExtAppnexus{ PlacementID: int(appnexusExt.PlacementId), TrafficSourceCode: appnexusExt.TrafficSourceCode, - Keywords: makeKeywordStr(appnexusExt.Keywords), + Keywords: appnexusExt.Keywords.String(), UsePmtRule: appnexusExt.UsePaymentRule, PrivateSizes: appnexusExt.PrivateSizes, + ExtInvCode: appnexusExt.ExtInvCode, + ExternalImpID: appnexusExt.ExternalImpId, }} var err error - if imp.Ext, err = json.Marshal(&impExt); err != nil { - return err - } - - return nil -} - -func makeKeywordStr(keywords []*openrtb_ext.ExtImpAppnexusKeyVal) string { - kvs := make([]string, 0, len(keywords)*2) - for _, kv := range keywords { - if len(kv.Values) == 0 { - kvs = append(kvs, kv.Key) - } else { - for _, val := range kv.Values { - kvs = append(kvs, fmt.Sprintf("%s=%s", kv.Key, val)) - } - } - } + imp.Ext, err = json.Marshal(&impExt) - return strings.Join(kvs, ",") + return err } // getMediaTypeForBid determines which type of bid. -func getMediaTypeForBid(bid *appnexusBidExt) (openrtb_ext.BidType, error) { +func getMediaTypeForBid(bid *bidExt) (openrtb_ext.BidType, error) { switch bid.Appnexus.BidType { case 0: return openrtb_ext.BidTypeBanner, nil case 1: return openrtb_ext.BidTypeVideo, nil - case 2: - return openrtb_ext.BidTypeAudio, nil case 3: return openrtb_ext.BidTypeNative, nil default: @@ -396,18 +419,17 @@ func getMediaTypeForBid(bid *appnexusBidExt) (openrtb_ext.BidType, error) { } // getIabCategoryForBid maps an appnexus brand id to an IAB category. -func (a *adapter) findIabCategoryForBid(bid *appnexusBidExt) (string, bool) { +func (a *adapter) findIabCategoryForBid(bid *bidExt) (string, bool) { brandIDString := strconv.Itoa(bid.Appnexus.BrandCategory) - iabCategory, ok := a.iabCategoryMap[brandIDString] + iabCategory, ok := iabCategoryMap[brandIDString] return iabCategory, ok } -func appendMemberId(uri string, memberId string) string { - if strings.Contains(uri, "?") { - return uri + "&member_id=" + memberId - } - - return uri + "?member_id=" + memberId +func appendMemberId(uri url.URL, memberId string) url.URL { + q := uri.Query() + q.Set("member_id", memberId) + uri.RawQuery = q.Encode() + return uri } func buildDisplayManageVer(req *openrtb2.BidRequest) string { @@ -428,14 +450,49 @@ func buildDisplayManageVer(req *openrtb2.BidRequest) string { return fmt.Sprintf("%s-%s", source, version) } -func (a *adapter) buildAdPodRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExtension appnexusReqExt, uri string) ([]*adapters.RequestData, []error) { +// moveSupplyChain moves the supply chain object from source.ext.schain to ext.schain. +func moveSupplyChain(request *openrtb2.BidRequest, extMap map[string]json.RawMessage) error { + if request == nil || request.Source == nil || len(request.Source.Ext) == 0 { + return nil + } + + sourceExtMap := make(map[string]json.RawMessage) + if err := json.Unmarshal(request.Source.Ext, &sourceExtMap); err != nil { + return err + } + + schainJson, exists := sourceExtMap["schain"] + if !exists { + return nil + } + + delete(sourceExtMap, "schain") + + request.Source = ptrutil.Clone(request.Source) + + if len(sourceExtMap) > 0 { + ext, err := json.Marshal(sourceExtMap) + if err != nil { + return err + } + request.Source.Ext = ext + } else { + request.Source.Ext = nil + } + + extMap["schain"] = schainJson + + return nil +} + +func (a *adapter) buildAdPodRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) { var errs []error podImps := groupByPods(imps) requests := make([]*adapters.RequestData, 0, len(podImps)) for _, podImps := range podImps { - requestExtension.Appnexus.AdPodId = fmt.Sprint(a.randomGenerator.GenerateInt63()) + requestExtAppnexus.AdPodID = fmt.Sprint(a.randomGenerator.GenerateInt63()) - reqs, errors := splitRequests(podImps, request, requestExtension, uri) + reqs, errors := splitRequests(podImps, request, requestExt, requestExtAppnexus, uri) requests = append(requests, reqs...) errs = append(errs, errors...) } diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go index 6797f4bbe9f..bb04b61bb97 100644 --- a/adapters/appnexus/appnexus_test.go +++ b/adapters/appnexus/appnexus_test.go @@ -1,11 +1,14 @@ package appnexus import ( + "net/url" "testing" "github.com/prebid/prebid-server/adapters/adapterstest" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJsonSamples(t *testing.T) { @@ -22,15 +25,24 @@ func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "appnexustest", bidder) } -func TestMemberQueryParam(t *testing.T) { - uriWithMember := appendMemberId("http://ib.adnxs.com/openrtb2?query_param=true", "102") - expected := "http://ib.adnxs.com/openrtb2?query_param=true&member_id=102" - if uriWithMember != expected { - t.Errorf("appendMemberId() failed on URI with query string. Expected %s, got %s", expected, uriWithMember) - } +func TestAppendMemberID(t *testing.T) { + uri, err := url.Parse("http://ib.adnxs.com/openrtb2?query_param=true") + require.NoError(t, err, "Failed to parse URI with query string") + + uriWithMember := appendMemberId(*uri, "102") + expected := "http://ib.adnxs.com/openrtb2?member_id=102&query_param=true" + assert.Equal(t, expected, uriWithMember.String(), "Failed to append member id to URI with query string") +} + +func TestBuilderWithPlatformID(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAppnexus, config.Adapter{ + Endpoint: "http://ib.adnxs.com/openrtb2", PlatformID: "3"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.NoError(t, buildErr) + require.NotNil(t, bidder) + assert.Equal(t, 3, (*bidder.(*adapter)).hbSource) } -// fakerandomNumberGenerator type FakeRandomNumberGenerator struct { Number int64 } diff --git a/adapters/appnexus/appnexustest/amp/simple-banner.json b/adapters/appnexus/appnexustest/amp/simple-banner.json index 54e6a143e19..4ed3403db7a 100644 --- a/adapters/appnexus/appnexustest/amp/simple-banner.json +++ b/adapters/appnexus/appnexustest/amp/simple-banner.json @@ -34,8 +34,6 @@ "appnexus": { "is_amp": 1, "hb_source": 5 - }, - "prebid": { } }, "id": "test-request-id", diff --git a/adapters/appnexus/appnexustest/amp/simple-video.json b/adapters/appnexus/appnexustest/amp/simple-video.json index 061d5c94369..623085e72a0 100644 --- a/adapters/appnexus/appnexustest/amp/simple-video.json +++ b/adapters/appnexus/appnexustest/amp/simple-video.json @@ -30,8 +30,6 @@ "appnexus": { "is_amp": 1, "hb_source": 5 - }, - "prebid": { } }, "id": "test-request-id", @@ -113,7 +111,7 @@ "crid": "29681110", "w": 300, "h": 250, - "cat": ["IAB5-3"], + "cat": ["IAB5-1"], "ext": { "appnexus": { "brand_id": 9, diff --git a/adapters/appnexus/appnexustest/exemplary/dynamic-keywords-params.json b/adapters/appnexus/appnexustest/exemplary/dynamic-keywords-params.json new file mode 100644 index 00000000000..d0af71e648a --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/dynamic-keywords-params.json @@ -0,0 +1,146 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "member": "103", + "inv_code": "abc", + "reserve": 20, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": {"abc": ["bas"]}, + "use_pmt_rule": true, + "private_sizes": [{"w": 300, "h": 250}] + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2?member_id=103", + "body": { + "id": "test-request-id", + "ext": { + "appnexus": { + "hb_source": 5 + } + }, + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [ + {"w":300,"h":250}, + {"w":300,"h":600} + ], + "w": 300, + "h": 250, + "pos": 3 + }, + "tagid": "abc", + "bidfloor": 20, + "ext": { + "appnexus": { + "keywords": "abc=bas", + "traffic_source_code": "trafficSource", + "use_pmt_rule": true, + "private_sizes": [{"w": 300, "h": 250}] + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": ["IAB20-3"], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/exemplary/native-1.1.json b/adapters/appnexus/appnexustest/exemplary/native-1.1.json index 189304fdb4c..b40543f12e9 100644 --- a/adapters/appnexus/appnexustest/exemplary/native-1.1.json +++ b/adapters/appnexus/appnexustest/exemplary/native-1.1.json @@ -51,8 +51,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "site": { "domain": "prebid.org", diff --git a/adapters/appnexus/appnexustest/exemplary/optional-params.json b/adapters/appnexus/appnexustest/exemplary/optional-params.json index ae7e9ff31a8..01af8252ba9 100644 --- a/adapters/appnexus/appnexustest/exemplary/optional-params.json +++ b/adapters/appnexus/appnexustest/exemplary/optional-params.json @@ -28,7 +28,9 @@ {"key": "valueless"} ], "use_pmt_rule": true, - "private_sizes": [{"w": 300, "h": 250}] + "private_sizes": [{"w": 300, "h": 250}], + "ext_inv_code": "invCode", + "external_imp_id": "impId" } } } @@ -44,8 +46,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { @@ -66,7 +67,9 @@ "keywords": "foo=bar,foo=baz,valueless", "traffic_source_code": "trafficSource", "use_pmt_rule": true, - "private_sizes": [{"w": 300, "h": 250}] + "private_sizes": [{"w": 300, "h": 250}], + "ext_inv_code": "invCode", + "external_imp_id": "impId" } } } diff --git a/adapters/appnexus/appnexustest/exemplary/schain-24-other-ext.json b/adapters/appnexus/appnexustest/exemplary/schain-24-other-ext.json new file mode 100644 index 00000000000..e5a258e7f35 --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/schain-24-other-ext.json @@ -0,0 +1,193 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement_id": 1, + "position": "above" + } + } + } + ], + "ext": { + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + }, + "otherRequest": "value" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "ext": { + "appnexus": { + "hb_source": 5 + }, + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + }, + "otherRequest": "value" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "pos": 1, + "h": 250 + }, + "displaymanagerver": "source-version", + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/exemplary/schain-24.json b/adapters/appnexus/appnexustest/exemplary/schain-24.json new file mode 100644 index 00000000000..d8f26d10a07 --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/schain-24.json @@ -0,0 +1,191 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement_id": 1, + "position": "above" + } + } + } + ], + "ext": { + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "ext": { + "appnexus": { + "hb_source": 5 + }, + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "pos": 1, + "h": 250 + }, + "displaymanagerver": "source-version", + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/exemplary/schain-25-other-ext.json b/adapters/appnexus/appnexustest/exemplary/schain-25-other-ext.json new file mode 100644 index 00000000000..19b9f1db6c4 --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/schain-25-other-ext.json @@ -0,0 +1,203 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement_id": 1, + "position": "above" + } + } + } + ], + "source": { + "ext": { + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + }, + "otherSource": "value" + } + }, + "ext": { + "otherRequest": "value" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "ext": { + "appnexus": { + "hb_source": 5 + }, + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + }, + "otherRequest": "value" + }, + "source": { + "ext": { + "otherSource": "value" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "pos": 1, + "h": 250 + }, + "displaymanagerver": "source-version", + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/exemplary/schain-25.json b/adapters/appnexus/appnexustest/exemplary/schain-25.json new file mode 100644 index 00000000000..8ff11b0122c --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/schain-25.json @@ -0,0 +1,194 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement_id": 1, + "position": "above" + } + } + } + ], + "source": { + "ext": { + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "ext": { + "appnexus": { + "hb_source": 5 + }, + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" + } + }, + "source": {}, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "pos": 1, + "h": 250 + }, + "displaymanagerver": "source-version", + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json b/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json index b46c6f5f76f..52112419a8d 100644 --- a/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json +++ b/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json @@ -34,8 +34,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner.json b/adapters/appnexus/appnexustest/exemplary/simple-banner.json index 15a7666e3ba..24deea82428 100644 --- a/adapters/appnexus/appnexustest/exemplary/simple-banner.json +++ b/adapters/appnexus/appnexustest/exemplary/simple-banner.json @@ -1,6 +1,14 @@ { "mockBidRequest": { "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, "imp": [ { "id": "test-imp-id", @@ -32,11 +40,18 @@ "uri": "http://ib.adnxs.com/openrtb2", "body": { "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { @@ -56,6 +71,7 @@ "pos": 1, "h": 250 }, + "displaymanagerver": "source-version", "ext": { "appnexus": { "placement_id": 1 diff --git a/adapters/appnexus/appnexustest/exemplary/simple-video.json b/adapters/appnexus/appnexustest/exemplary/simple-video.json index ced90c39549..beb630104ca 100644 --- a/adapters/appnexus/appnexustest/exemplary/simple-video.json +++ b/adapters/appnexus/appnexustest/exemplary/simple-video.json @@ -30,8 +30,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { @@ -111,7 +110,7 @@ "crid": "29681110", "w": 300, "h": 250, - "cat": ["IAB5-3"], + "cat": ["IAB5-1"], "ext": { "appnexus": { "brand_id": 9, diff --git a/adapters/appnexus/appnexustest/exemplary/string-keywords-params.json b/adapters/appnexus/appnexustest/exemplary/string-keywords-params.json new file mode 100644 index 00000000000..a6d8d95eb5c --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/string-keywords-params.json @@ -0,0 +1,146 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "member": "103", + "inv_code": "abc", + "reserve": 20, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": "foo=bar,foo=baz,valueless", + "use_pmt_rule": true, + "private_sizes": [{"w": 300, "h": 250}] + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2?member_id=103", + "body": { + "id": "test-request-id", + "ext": { + "appnexus": { + "hb_source": 5 + } + }, + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [ + {"w":300,"h":250}, + {"w":300,"h":600} + ], + "w": 300, + "h": 250, + "pos": 3 + }, + "tagid": "abc", + "bidfloor": 20, + "ext": { + "appnexus": { + "keywords": "foo=bar,foo=baz,valueless", + "traffic_source_code": "trafficSource", + "use_pmt_rule": true, + "private_sizes": [{"w": 300, "h": 250}] + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": ["IAB20-3"], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json index 257905c873f..a65595e1bc0 100644 --- a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json +++ b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json @@ -30,8 +30,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json index c6ad330e3a8..ebfa1f239fe 100644 --- a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json +++ b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json @@ -50,8 +50,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json b/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json index 685a908b4f7..c66f897349f 100644 --- a/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json +++ b/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json @@ -31,8 +31,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/brightroll/brightrolltest/exemplary/simple-banner.json b/adapters/appnexus/appnexustest/supplemental/invalid-bid-type.json similarity index 54% rename from adapters/brightroll/brightrolltest/exemplary/simple-banner.json rename to adapters/appnexus/appnexustest/supplemental/invalid-bid-type.json index f59038503cf..a66f69e358e 100644 --- a/adapters/brightroll/brightrolltest/exemplary/simple-banner.json +++ b/adapters/appnexus/appnexustest/supplemental/invalid-bid-type.json @@ -1,6 +1,14 @@ { "mockBidRequest": { "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, "imp": [ { "id": "test-imp-id", @@ -18,7 +26,8 @@ }, "ext": { "bidder": { - "publisher": "adthrive" + "placement_id": 1, + "position": "above" } } } @@ -28,23 +37,26 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", + "uri": "http://ib.adnxs.com/openrtb2", "body": { "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "ext": { + "appnexus": { + "hb_source": 5 + } + }, "imp": [ { "id": "test-imp-id", "banner": { - "battr": [ - 1, - 2, - 3 - ], "format": [ { "w": 300, @@ -56,11 +68,13 @@ } ], "w": 300, + "pos": 1, "h": 250 }, + "displaymanagerver": "source-version", "ext": { - "bidder": { - "publisher": "adthrive" + "appnexus": { + "placement_id": 1 } } } @@ -80,11 +94,23 @@ "price": 0.500000, "adid": "29681110", "adm": "some-test-ad", - "adomain": ["yahoo.com"], + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", "cid": "958", "crid": "29681110", "h": 250, - "w": 300 + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 2, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } }] } ], @@ -94,26 +120,11 @@ } } ], - - "expectedBidResponses": [ + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ { - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250 - }, - "type": "banner" - } - ] + "value": "Unrecognized bid_ad_type in response from appnexus: 2", + "comparison": "literal" } ] } diff --git a/adapters/appnexus/appnexustest/supplemental/legacy-params.json b/adapters/appnexus/appnexustest/supplemental/legacy-params.json index 82031145b7a..15a4c54587a 100644 --- a/adapters/appnexus/appnexustest/supplemental/legacy-params.json +++ b/adapters/appnexus/appnexustest/supplemental/legacy-params.json @@ -37,8 +37,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/multi-bid.json b/adapters/appnexus/appnexustest/supplemental/multi-bid.json index 9e63bdced95..664244a9850 100644 --- a/adapters/appnexus/appnexustest/supplemental/multi-bid.json +++ b/adapters/appnexus/appnexustest/supplemental/multi-bid.json @@ -34,8 +34,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json b/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json index ce56b01e193..2d9fe9ceaa9 100644 --- a/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json +++ b/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json @@ -58,73 +58,10 @@ } ] }, - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://ib.adnxs.com/openrtb2?member_id=103", - "body": { - "id": "test-request-id", - "ext": { - "appnexus": { - "hb_source": 5, - "brand_category_uniqueness": true, - "include_brand_category": true - }, - "prebid": { - "targeting": {"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includebidderkeys":true,"includewinners":true,"includebrandcategory":{"primaryadserver":1,"publisher":"","withcategory":true}} - } - }, - "imp": [ - { - "id":"test-imp-id", - "banner": { - "format": [ - {"w":300,"h":250}, - {"w":300,"h":600} - ], - "w": 300, - "h": 250, - "pos": 3 - }, - "tagid": "abc", - "bidfloor": 20, - "ext": { - "appnexus": { - "use_pmt_rule": true - } - } - }, - { - "id":"test-imp-id-2", - "banner": { - "format": [ - {"w":300,"h":250}, - {"w":300,"h":600} - ], - "w": 300, - "h": 250, - "pos": 3 - }, - "tagid": "abc", - "bidfloor": 20, - "ext": { - "appnexus": { - "use_pmt_rule": true - } - } - } - ] - } - }, - "mockResponse": { - "status": 204 - } - } - ], "expectedBidResponses": [], "expectedMakeRequestsErrors": [ { - "value": "All request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained: [103 104]", + "value": "all request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained member IDs 103 and 104", "comparison": "literal" } ] diff --git a/adapters/appnexus/appnexustest/supplemental/placement-id-as-string-test.json b/adapters/appnexus/appnexustest/supplemental/placement-id-as-string-test.json index 1d28983ba7c..b1e0401ecfc 100644 --- a/adapters/appnexus/appnexustest/supplemental/placement-id-as-string-test.json +++ b/adapters/appnexus/appnexustest/supplemental/placement-id-as-string-test.json @@ -51,8 +51,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "site": { "domain": "prebid.org", diff --git a/adapters/appnexus/appnexustest/supplemental/request-ext-appnexus-existing.json b/adapters/appnexus/appnexustest/supplemental/request-ext-appnexus-existing.json new file mode 100644 index 00000000000..0f153117550 --- /dev/null +++ b/adapters/appnexus/appnexustest/supplemental/request-ext-appnexus-existing.json @@ -0,0 +1,171 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement_id": 1, + "position": "above" + } + } + } + ], + "ext": { + "appnexus": { + "adpod_id": "42" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "app": { + "ext": { + "prebid": { + "source": "source", + "version": "version" + } + } + }, + "ext": { + "appnexus": { + "hb_source": 5, + "adpod_id": "42" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "pos": 1, + "h": 250 + }, + "displaymanagerver": "source-version", + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/supplemental/reserve-ignored.json b/adapters/appnexus/appnexustest/supplemental/reserve-ignored.json index 84e2b48484e..e9df5038c1d 100644 --- a/adapters/appnexus/appnexustest/supplemental/reserve-ignored.json +++ b/adapters/appnexus/appnexustest/supplemental/reserve-ignored.json @@ -36,8 +36,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/reserve-test.json b/adapters/appnexus/appnexustest/supplemental/reserve-test.json index f5c6dfceb4f..df861897d74 100644 --- a/adapters/appnexus/appnexustest/supplemental/reserve-test.json +++ b/adapters/appnexus/appnexustest/supplemental/reserve-test.json @@ -35,8 +35,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/status-400.json b/adapters/appnexus/appnexustest/supplemental/status-400.json index dc88901231a..58c7a465219 100644 --- a/adapters/appnexus/appnexustest/supplemental/status-400.json +++ b/adapters/appnexus/appnexustest/supplemental/status-400.json @@ -35,8 +35,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/supplemental/usePmtRule-test.json b/adapters/appnexus/appnexustest/supplemental/usePmtRule-test.json index 64e519273ab..2d3f307a5a8 100644 --- a/adapters/appnexus/appnexustest/supplemental/usePmtRule-test.json +++ b/adapters/appnexus/appnexustest/supplemental/usePmtRule-test.json @@ -63,8 +63,7 @@ "ext": { "appnexus": { "hb_source": 5 - }, - "prebid": {} + } }, "imp": [ { diff --git a/adapters/appnexus/appnexustest/video/video-brand-category.json b/adapters/appnexus/appnexustest/video/video-brand-category.json new file mode 100644 index 00000000000..1671b97171d --- /dev/null +++ b/adapters/appnexus/appnexustest/video/video-brand-category.json @@ -0,0 +1,179 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "placement_id": 1 + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "anyPublisher", + "withcategory": true + } + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "ext": { + "appnexus": { + "hb_source": 6, + "brand_category_uniqueness": true, + "include_brand_category": true + }, + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "anyPublisher", + "withcategory": true + } + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "cat": [ + "IAB9-1" + ], + "ext": { + "appnexus": { + "brand_id": 9, + "brand_category_id": 9, + "auction_id": 8189378542222915032, + "bid_ad_type": 1, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB5-1" + ], + "ext": { + "appnexus": { + "brand_id": 9, + "brand_category_id": 9, + "auction_id": 8189378542222915032, + "bid_ad_type": 1, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json b/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json index 140cf0b0372..811cfcb2866 100644 --- a/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json +++ b/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json @@ -46,8 +46,7 @@ "ext": { "appnexus": { "hb_source": 6 - }, - "prebid": {} + } }, "imp": [ { @@ -90,5 +89,6 @@ "body": {} } } - ] - } \ No newline at end of file + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}] + } diff --git a/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json index 86fa14aac1a..8f0f15262c4 100644 --- a/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json +++ b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json @@ -49,8 +49,7 @@ "appnexus": { "adpod_id": "10", "hb_source": 6 - }, - "prebid": {} + } }, "imp": [ { @@ -93,5 +92,6 @@ "body": {} } } - ] - } \ No newline at end of file + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}] + } diff --git a/adapters/appnexus/iab_categories.go b/adapters/appnexus/iab_categories.go new file mode 100644 index 00000000000..c9c1c65c04a --- /dev/null +++ b/adapters/appnexus/iab_categories.go @@ -0,0 +1,99 @@ +package appnexus + +var iabCategoryMap = map[string]string{ + "1": "IAB20-3", + "2": "IAB18-5", + "3": "IAB10-1", + "4": "IAB2-3", + "5": "IAB19-8", + "6": "IAB22-1", + "7": "IAB18-1", + "8": "IAB12-3", + "9": "IAB5-1", + "10": "IAB4-5", + "11": "IAB13-4", + "12": "IAB8-7", + "13": "IAB9-7", + "14": "IAB7-1", + "15": "IAB20-18", + "16": "IAB10-7", + "17": "IAB19-18", + "18": "IAB13-6", + "19": "IAB18-4", + "20": "IAB1-5", + "21": "IAB1-6", + "22": "IAB3-4", + "23": "IAB19-13", + "24": "IAB22-2", + "25": "IAB3-9", + "26": "IAB17-18", + "27": "IAB19-6", + "28": "IAB1-7", + "29": "IAB9-30", + "30": "IAB20-7", + "31": "IAB20-17", + "32": "IAB7-32", + "33": "IAB16-5", + "34": "IAB19-34", + "35": "IAB11-5", + "36": "IAB12-3", + "37": "IAB11-4", + "38": "IAB12-3", + "39": "IAB9-30", + "41": "IAB7-44", + "42": "IAB7-1", + "43": "IAB7-30", + "50": "IAB19-30", + "51": "IAB17-12", + "52": "IAB19-30", + "53": "IAB3-1", + "55": "IAB13-2", + "56": "IAB19-30", + "57": "IAB19-30", + "58": "IAB7-39", + "59": "IAB22-1", + "60": "IAB7-39", + "61": "IAB21-3", + "62": "IAB5-1", + "63": "IAB12-3", + "64": "IAB20-18", + "65": "IAB11-2", + "66": "IAB17-18", + "67": "IAB9-9", + "68": "IAB9-5", + "69": "IAB7-44", + "71": "IAB22-3", + "73": "IAB19-30", + "74": "IAB8-5", + "78": "IAB22-1", + "85": "IAB12-2", + "86": "IAB22-3", + "87": "IAB11-3", + "112": "IAB7-32", + "113": "IAB7-32", + "114": "IAB7-32", + "115": "IAB7-32", + "118": "IAB9-5", + "119": "IAB9-5", + "120": "IAB9-5", + "121": "IAB9-5", + "122": "IAB9-5", + "123": "IAB9-5", + "124": "IAB9-5", + "125": "IAB9-5", + "126": "IAB9-5", + "127": "IAB22-1", + "132": "IAB1-2", + "133": "IAB19-30", + "137": "IAB3-9", + "138": "IAB19-3", + "140": "IAB2-3", + "141": "IAB2-1", + "142": "IAB2-3", + "143": "IAB17-13", + "166": "IAB11-4", + "175": "IAB3-1", + "176": "IAB13-4", + "182": "IAB8-9", + "183": "IAB3-5", +} diff --git a/adapters/appnexus/models.go b/adapters/appnexus/models.go index a3f6be99b9f..c70f89faa6c 100644 --- a/adapters/appnexus/models.go +++ b/adapters/appnexus/models.go @@ -2,52 +2,46 @@ package appnexus import ( "encoding/json" - - "github.com/prebid/prebid-server/openrtb_ext" ) -type appnexusImpExtAppnexus struct { +type impExtAppnexus struct { PlacementID int `json:"placement_id,omitempty"` Keywords string `json:"keywords,omitempty"` TrafficSourceCode string `json:"traffic_source_code,omitempty"` UsePmtRule *bool `json:"use_pmt_rule,omitempty"` PrivateSizes json.RawMessage `json:"private_sizes,omitempty"` + ExtInvCode string `json:"ext_inv_code,omitempty"` + ExternalImpID string `json:"external_imp_id,omitempty"` } -type appnexusImpExt struct { - Appnexus appnexusImpExtAppnexus `json:"appnexus"` +type impExt struct { + Appnexus impExtAppnexus `json:"appnexus"` } -type appnexusBidExtVideo struct { +type bidExtVideo struct { Duration int `json:"duration"` } -type appnexusBidExtCreative struct { - Video appnexusBidExtVideo `json:"video"` +type bidExtCreative struct { + Video bidExtVideo `json:"video"` } -type appnexusBidExtAppnexus struct { - BidType int `json:"bid_ad_type"` - BrandId int `json:"brand_id"` - BrandCategory int `json:"brand_category_id"` - CreativeInfo appnexusBidExtCreative `json:"creative_info"` - DealPriority int `json:"deal_priority"` +type bidExtAppnexus struct { + BidType int `json:"bid_ad_type"` + BrandId int `json:"brand_id"` + BrandCategory int `json:"brand_category_id"` + CreativeInfo bidExtCreative `json:"creative_info"` + DealPriority int `json:"deal_priority"` } -type appnexusBidExt struct { - Appnexus appnexusBidExtAppnexus `json:"appnexus"` +type bidExt struct { + Appnexus bidExtAppnexus `json:"appnexus"` } -type appnexusReqExtAppnexus struct { +type bidReqExtAppnexus struct { IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` IsAMP int `json:"is_amp,omitempty"` HeaderBiddingSource int `json:"hb_source,omitempty"` - AdPodId string `json:"adpod_id,omitempty"` -} - -// Full request extension including appnexus extension object -type appnexusReqExt struct { - openrtb_ext.ExtRequest - Appnexus *appnexusReqExtAppnexus `json:"appnexus,omitempty"` + AdPodID string `json:"adpod_id,omitempty"` } diff --git a/adapters/appnexus/params_test.go b/adapters/appnexus/params_test.go index d12fba124fb..ed9adedcc6b 100644 --- a/adapters/appnexus/params_test.go +++ b/adapters/appnexus/params_test.go @@ -50,7 +50,11 @@ var validParams = []string{ `{"placementId":123, "keywords":[{"key":"foo","value":["bar"]}]}`, `{"placement_id":123, "keywords":[{"key":"foo","value":["bar", "baz"]}]}`, `{"placement_id":123, "keywords":[{"key":"foo"}]}`, + `{"placement_id":123, "keywords":"foo=bar,foo=baz"}`, + `{"placement_id":123, "keywords":{"genre": ["rock", "pop"], "pets": ["dog"]}}`, `{"placement_id":123, "use_pmt_rule": true, "private_sizes": [{"w": 300, "h":250}]}`, + `{"placementId":123, "ext_inv_code": "invCode"}`, + `{"placementId":123, "external_imp_id": "impId"}`, } var invalidParams = []string{ @@ -72,7 +76,10 @@ var invalidParams = []string{ `{"placement_id":123, "keywords":["foo"]}`, `{"placementId":123, "keywords":[{"key":"foo","value":[]}]}`, `{"placementId":123, "keywords":[{"value":["bar"]}]}`, + `{"placement_id":123, "keywords":{"genre": [12]}}`, `{"placement_id":123, "use_pmt_rule": "true"}`, `{"placement_id":123, "private_sizes": [[300,250]]}`, `{"placement_id":123, "private_sizes": [{"w": "300", "h": "250"}]}`, + `{"placementId":123, "ext_inv_code": 1}`, + `{"placementId":123, "external_imp_id": 2}`, } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json index c33807bda74..a5a9384edc5 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json @@ -96,8 +96,9 @@ } } }], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [{ "value": "invalid character 'm' looking for beginning of value", "comparison": "literal" }] -} \ No newline at end of file +} diff --git a/adapters/avocet/avocet_test.go b/adapters/avocet/avocet_test.go index b8a38ac1b62..1dcf81c7d78 100644 --- a/adapters/avocet/avocet_test.go +++ b/adapters/avocet/avocet_test.go @@ -23,7 +23,7 @@ func TestJsonSamples(t *testing.T) { t.Fatalf("Builder returned unexpected error %v", buildErr) } - adapterstest.RunJSONBidderTest(t, "avocet", bidder) + adapterstest.RunJSONBidderTest(t, "avocettest", bidder) } func TestAvocetAdapter_MakeRequests(t *testing.T) { diff --git a/adapters/avocet/avocettest/exemplary/video.json b/adapters/avocet/avocettest/exemplary/video.json index 2398256b0dd..64c3fa3bd73 100644 --- a/adapters/avocet/avocettest/exemplary/video.json +++ b/adapters/avocet/avocettest/exemplary/video.json @@ -77,28 +77,34 @@ } } ], - - "expectedBids": [ + "expectedBidResponses": [ { - "bid": { - "adm": "Avocet", - "adomain": ["avocet.io"], - "cid": "5b51e2d689654741306813a4", - "crid": "5ec530e32d57fe1100f17d87", - "h": 396, - "id": "3d4c2d45-5a8c-43b8-9e15-4f48ac45204f", - "impid": "dfp-ad--top-above-nav", - "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5ec530e32d57fe1100f17d87.jpeg", - "language": "en", - "price": 15.64434783, - "w": 600, - "ext": { - "avocet": { - "duration": 30 - } + "currency": "EUR", + "bids": [ + { + "bid": { + "adm": "Avocet", + "adomain": [ + "avocet.io" + ], + "cid": "5b51e2d689654741306813a4", + "crid": "5ec530e32d57fe1100f17d87", + "ext": { + "avocet": { + "duration": 30 + } + }, + "h": 396, + "id": "3d4c2d45-5a8c-43b8-9e15-4f48ac45204f", + "impid": "dfp-ad--top-above-nav", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5ec530e32d57fe1100f17d87.jpeg", + "language": "en", + "price": 15.64434783, + "w": 600 + }, + "type": "video" } - }, - "type": "video" + ] } ] } diff --git a/adapters/axis/axis.go b/adapters/axis/axis.go new file mode 100644 index 00000000000..f03a572e625 --- /dev/null +++ b/adapters/axis/axis.go @@ -0,0 +1,141 @@ +package axis + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + AxisBidderExt openrtb_ext.ImpExtAxis `json:"bidder"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var adapterRequests []*adapters.RequestData + + originalImpSlice := request.Imp + + for i := range request.Imp { + currImp := originalImpSlice[i] + request.Imp = []openrtb2.Imp{currImp} + + var bidderExt reqBodyExt + if err := json.Unmarshal(currImp.Ext, &bidderExt); err != nil { + continue + } + + extJson, err := json.Marshal(bidderExt) + if err != nil { + return nil, []error{err} + } + + request.Imp[0].Ext = extJson + + adapterReq, err := a.buildRequest(request) + if err != nil { + return nil, []error{err} + } + + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + } + request.Imp = originalImpSlice + return adapterRequests, nil +} + +func (a *adapter) buildRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode), + } + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + + impsMappedByID := make(map[string]openrtb2.Imp, len(request.Imp)) + for i, imp := range request.Imp { + impsMappedByID[request.Imp[i].ID] = imp + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, impsMappedByID) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getMediaTypeForImp(impID string, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) { + if index, found := impMap[impID]; found { + if index.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + if index.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } + if index.Native != nil { + return openrtb_ext.BidTypeNative, nil + } + } + + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\"", impID), + } +} diff --git a/adapters/axis/axis_test.go b/adapters/axis/axis_test.go new file mode 100644 index 00000000000..56cb7f0b6e8 --- /dev/null +++ b/adapters/axis/axis_test.go @@ -0,0 +1,20 @@ +package axis + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAxis, config.Adapter{ + Endpoint: "http://prebid.axis-marketplace.com/pbserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "axistest", bidder) +} diff --git a/adapters/axis/axistest/exemplary/simple-banner.json b/adapters/axis/axistest/exemplary/simple-banner.json new file mode 100644 index 00000000000..2c276c49b6d --- /dev/null +++ b/adapters/axis/axistest/exemplary/simple-banner.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "Test", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "Test", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/axis/axistest/exemplary/simple-native.json b/adapters/axis/axistest/exemplary/simple-native.json new file mode 100644 index 00000000000..ce337ec64a2 --- /dev/null +++ b/adapters/axis/axistest/exemplary/simple-native.json @@ -0,0 +1,118 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "{}", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "{}", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/axis/axistest/exemplary/simple-video.json b/adapters/axis/axistest/exemplary/simple-video.json new file mode 100644 index 00000000000..291a4f42762 --- /dev/null +++ b/adapters/axis/axistest/exemplary/simple-video.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/axis/axistest/exemplary/simple-web-banner.json b/adapters/axis/axistest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..06ba5bc5d54 --- /dev/null +++ b/adapters/axis/axistest/exemplary/simple-web-banner.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "Test", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "Test", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/axis/axistest/supplemental/bad_media_type.json b/adapters/axis/axistest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..bf26adc5418 --- /dev/null +++ b/adapters/axis/axistest/supplemental/bad_media_type.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "Test", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Failed to find impression \"test-imp-id\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/axis/axistest/supplemental/bad_response.json b/adapters/axis/axistest/supplemental/bad_response.json new file mode 100644 index 00000000000..ae1ad6a6a52 --- /dev/null +++ b/adapters/axis/axistest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/axis/axistest/supplemental/status-204.json b/adapters/axis/axistest/supplemental/status-204.json new file mode 100644 index 00000000000..db8cdf2a748 --- /dev/null +++ b/adapters/axis/axistest/supplemental/status-204.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/axis/axistest/supplemental/status-not-200.json b/adapters/axis/axistest/supplemental/status-not-200.json new file mode 100644 index 00000000000..2d27dcfc674 --- /dev/null +++ b/adapters/axis/axistest/supplemental/status-not-200.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://prebid.axis-marketplace.com/pbserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "integration": "000000", + "token": "000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info.", + "comparison": "literal" + } + ] +} diff --git a/adapters/axis/params_test.go b/adapters/axis/params_test.go new file mode 100644 index 00000000000..ba374c80b0f --- /dev/null +++ b/adapters/axis/params_test.go @@ -0,0 +1,54 @@ +package axis + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderAxis, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAxis, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", p) + } + } +} + +var validParams = []string{ + `{"integration":"000000", "token":"000000"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"anyparam": "anyvalue"}`, + `{"integration":"9Q20EdGxzgWdfPYShScl"}`, + `{"token":"Y9Evrh40ejsrCR4EtidUt1cSxhJsz8X1"}`, + `{"integration":"9Q20EdGxzgWdfPYShScl", "token":"alNYtemWggraDVbhJrsOs9pXc3Eld32E"}`, + `{"integration":"", "token":""}`, + `{"integration":"9Q20EdGxzgWdfPYShScl", "token":""}`, + `{"integration":"", "token":"alNYtemWggraDVbhJrsOs9pXc3Eld32E"}`, +} diff --git a/adapters/axonix/axonix.go b/adapters/axonix/axonix.go index 7413cec37a3..2289050b462 100644 --- a/adapters/axonix/axonix.go +++ b/adapters/axonix/axonix.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "net/url" + "strconv" + "strings" "text/template" "github.com/prebid/openrtb/v19/openrtb2" @@ -105,6 +107,7 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R for _, seatBid := range response.SeatBid { for _, bid := range seatBid.Bid { bid := bid + resolveMacros(&bid) b := &adapters.TypedBid{ Bid: &bid, BidType: getMediaType(bid.ImpID, request.Imp), @@ -129,3 +132,12 @@ func getMediaType(impId string, imps []openrtb2.Imp) openrtb_ext.BidType { } return openrtb_ext.BidTypeBanner } + +func resolveMacros(bid *openrtb2.Bid) { + if bid == nil { + return + } + price := strconv.FormatFloat(bid.Price, 'f', -1, 64) + bid.NURL = strings.Replace(bid.NURL, "${AUCTION_PRICE}", price, -1) + bid.AdM = strings.Replace(bid.AdM, "${AUCTION_PRICE}", price, -1) +} diff --git a/adapters/axonix/axonixtest/exemplary/simple-banner.json b/adapters/axonix/axonixtest/exemplary/simple-banner.json index 40441a46425..d1199b80929 100644 --- a/adapters/axonix/axonixtest/exemplary/simple-banner.json +++ b/adapters/axonix/axonixtest/exemplary/simple-banner.json @@ -67,7 +67,8 @@ "impid": "test-imp-id", "price": 0.500000, "adid": "29681110", - "adm": "some-test-ad", + "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}", + "nurl": "nurl.link/win?p=${AUCTION_PRICE}", "adomain": ["yahoo.com"], "cid": "958", "crid": "29681110", @@ -90,7 +91,8 @@ "id": "7706636740145184841", "impid": "test-imp-id", "price": 0.5, - "adm": "some-test-ad", + "adm": "some-test-ad imp_0.5 creativeview_0.5", + "nurl": "nurl.link/win?p=0.5", "adid": "29681110", "adomain": ["yahoo.com"], "cid": "958", diff --git a/adapters/beachfront/beachfronttest/supplemental/banner-and-adm-video-expected-204-response-on-banner.json b/adapters/beachfront/beachfronttest/supplemental/banner-and-adm-video-expected-204-response-on-banner.json index 9b0f1863d46..27ed881f0a8 100644 --- a/adapters/beachfront/beachfronttest/supplemental/banner-and-adm-video-expected-204-response-on-banner.json +++ b/adapters/beachfront/beachfronttest/supplemental/banner-and-adm-video-expected-204-response-on-banner.json @@ -148,7 +148,6 @@ ], "expectedBidResponses": [ - {"bids":[]}, { "bids": [ { diff --git a/adapters/beachfront/beachfronttest/supplemental/six-nine-combo.json b/adapters/beachfront/beachfronttest/supplemental/six-nine-combo.json index 294bd13d5f6..c75642dff8c 100644 --- a/adapters/beachfront/beachfronttest/supplemental/six-nine-combo.json +++ b/adapters/beachfront/beachfronttest/supplemental/six-nine-combo.json @@ -493,5 +493,111 @@ } } } + ], + "expectedBidResponses": [ + {"currency":"USD","bids":[ + {"bid": { + "adm": "
Buy now!
", + "crid": "crid_1", + "h": 150, + "id": "ADMVideoWithBannerImp1Banner", + "impid": "ADMVideoWithBannerImp1", + "price": 1.594764, + "w": 100 + }, "type": "banner"}, + {"bid": { + "adm": "
Buy now!
", + "crid": "crid_2", + "h": 150, + "id": "ADMVideoWithBannerImp2Banner", + "impid": "ADMVideoWithBannerImp2", + "price": 1.594764, + "w": 100 + }, "type": "banner"}, + {"bid": { + "adm": "
Buy now!
", + "crid": "crid_3", + "h": 1024, + "id": "RegularBannerImp4Banner", + "impid": "RegularBannerImp4", + "price": 1.501188, + "w": 1280 + }, "type": "banner"}, + {"bid": { + "adm": "
Buy now!
", + "crid": "crid_4", + "h": 1024, + "id": "RegularBannerImp5Banner", + "impid": "RegularBannerImp5", + "price": 1.501188, + "w": 1280 + }, "type": "banner"} + ]}, + {"currency":"USD","bids":[ + { + "bid": { + "id": "ADMVideoWithBannerImp1AdmVideo", + "impid": "ADMVideoWithBannerImp1", + "price": 0.8, + "adm": "http://example.com/vast.xml", + "cat":["IAB2"], + "w": 300, + "h": 400, + "ext": { + "duration": 30 + } + }, + "type": "video" + } + ]}, + {"currency":"USD","bids":[ + { + "bid": { + "id": "ADMVideoWithBannerImp2AdmVideo", + "impid": "ADMVideoWithBannerImp2", + "price": 0.8, + "adm": "http://example.com/vast.xml", + "cat":["IAB2"], + "w": 300, + "h": 400, + "ext": { + "duration": 30 + } + }, + "type": "video" + } + ]}, + {"currency":"USD","bids":[ + { + "bid": { + "id": "ADMVideoImp6AdmVideo", + "impid": "ADMVideoImp6", + "price": 0.8, + "adm": "http://example.com/vast.xml", + "cat":["IAB2"], + "w": 100, + "h": 150, + "ext": { + "duration": 30 + } + }, + "type": "video" + } + ]}, + {"currency":"USD","bids":[ + { + "bid": { + "crid": "de5b4976-0d2a-4032-874c-35e2b3e3ab95", + "id": "NURLVideoImp3NurlVideo", + "impid": "NURLVideoImp3", + "nurl": "https://useast.bfmio.com/getBids?aid=bid:de5b4976-0d2a-4032-874c-35e2b3e3ab95: videoAppId1: 10.0: 10.0&v=1&dsp=5f7da002b6b0ca361fc692b0,0.01&i_type=pre", + "price": 20, + "w": 150, + "h": 300, + "ext": {} + }, + "type": "video" + } + ]} ] } diff --git a/adapters/beachfront/beachfronttest/supplemental/two-four-combo.json b/adapters/beachfront/beachfronttest/supplemental/two-four-combo.json index 7310d0b4f19..a305034fc0f 100644 --- a/adapters/beachfront/beachfronttest/supplemental/two-four-combo.json +++ b/adapters/beachfront/beachfronttest/supplemental/two-four-combo.json @@ -203,7 +203,7 @@ "w":300, "h":400, "ext":{ - "duration":15 + "duration":30 } } ], @@ -269,5 +269,62 @@ } } } + ], + "expectedBidResponses": [ + {"currency":"USD","bids":[ + {"bid": { + "adm": "
", + "crid": "crid_1", + "h": 250, + "id": "div-gpt-ad-1460505748561-0Banner", + "impid": "div-gpt-ad-1460505748561-0", + "price": 2.942808, + "w": 300 + }, "type": "banner"}, + {"bid": { + "adm": "
", + "crid": "crid_1", + "h": 250, + "id": "div-gpt-ad-1460505748561-0Banner", + "impid": "div-gpt-ad-1460505748561-0", + "price": 2.942808, + "w": 300 + }, "type": "banner"} + ]}, + {"currency":"USD","bids":[ + { + "bid": { + "adid": "22873", + "adomain": ["nintendo.com"], + "id": "ComboADMVideoWithBannerImp1AdmVideo", + "impid": "ComboADMVideoWithBannerImp1", + "price": 2.5500002, + "adm": "http://example.com/vast.xml", + "cat":["IAB9-30"], + "cid": "6095", + "crid": "0", + "w": 300, + "h": 400, + "ext": { + "duration": 30 + } + }, + "type": "video" + } + ]}, + {"currency":"USD","bids":[ + { + "bid": { + "crid": "747f49a2-7a15-40bd-a43f-b8576eaa0368", + "id": "ComboNurlVideoWithBannerImp2NurlVideo", + "impid": "ComboNurlVideoWithBannerImp2", + "nurl": "https://useast.bfmio.com/getmu?aid=bid:747f49a2-7a15-40bd-a43f-b8576eaa0368:2096a4df-d32c-4b7b-b46c-f4b5d717327a:2.5500002:2.5500002&v=1&dsp=5fe7a6ab3f06cc571bf5c897,1.01&i_type=pre", + "price": 2.4700302, + "w": 100, + "h": 150 + }, + "type": "video" + } + ]} ] } diff --git a/adapters/beintoo/beintootest/supplemental/add-bidfloor.json b/adapters/beintoo/beintootest/supplemental/add-bidfloor.json index a53d11ff068..b578701bc24 100644 --- a/adapters/beintoo/beintootest/supplemental/add-bidfloor.json +++ b/adapters/beintoo/beintootest/supplemental/add-bidfloor.json @@ -39,5 +39,5 @@ } } }], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/beintoo/beintootest/supplemental/build-banner-object.json b/adapters/beintoo/beintootest/supplemental/build-banner-object.json index 32756ff69ce..0ad6cf955a3 100644 --- a/adapters/beintoo/beintootest/supplemental/build-banner-object.json +++ b/adapters/beintoo/beintootest/supplemental/build-banner-object.json @@ -58,5 +58,5 @@ } } }], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/beintoo/beintootest/supplemental/site-domain-and-url-correctly-parsed.json b/adapters/beintoo/beintootest/supplemental/site-domain-and-url-correctly-parsed.json index d9b9f6e3c18..1f8ba2e1783 100644 --- a/adapters/beintoo/beintootest/supplemental/site-domain-and-url-correctly-parsed.json +++ b/adapters/beintoo/beintootest/supplemental/site-domain-and-url-correctly-parsed.json @@ -58,5 +58,5 @@ } } ], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/bematterfull/bematterfull.go b/adapters/bematterfull/bematterfull.go new file mode 100644 index 00000000000..bf63e7c8c8b --- /dev/null +++ b/adapters/bematterfull/bematterfull.go @@ -0,0 +1,161 @@ +package bematterfull + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type bidType struct { + Type string `json:"type"` +} + +type bidExt struct { + Prebid bidType `json:"prebid"` +} + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + tmpl, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint URL template: %v", err) + } + + bidder := &adapter{ + endpoint: tmpl, + } + + return bidder, nil +} + +func (a *adapter) buildEndpointFromRequest(imp *openrtb2.Imp) (string, error) { + var impExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize bidder impression extension: %v", err), + } + } + + var bematterfullExt openrtb_ext.ExtBematterfull + if err := json.Unmarshal(impExt.Bidder, &bematterfullExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize Bematterfull extension: %v", err), + } + } + + endpointParams := macros.EndpointTemplateParams{ + Host: bematterfullExt.Env, + SourceId: bematterfullExt.Pid, + } + + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errs []error + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestCopy := *request + for _, imp := range request.Imp { + requestCopy.Imp = []openrtb2.Imp{imp} + + endpoint, err := a.buildEndpointFromRequest(&imp) + if err != nil { + errs = append(errs, err) + continue + } + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + request := &adapters.RequestData{ + Method: http.MethodPost, + Body: requestJSON, + Uri: endpoint, + Headers: headers, + } + + requests = append(requests, request) + } + + return requests, errs +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + if bidderRawResponse.StatusCode == http.StatusServiceUnavailable { + return nil, []error{&errortypes.BadInput{ + Message: "Bidder Bematterfull is unavailable. Please contact the bidder support.", + }} + } + + if err := adapters.CheckResponseStatusCodeForErrors(bidderRawResponse); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(bidderRawResponse.Body, &bidResp); err != nil { + return nil, []error{err} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Array SeatBid cannot be empty", + }} + } + + return prepareBidResponse(bidResp.SeatBid) +} + +func prepareBidResponse(seats []openrtb2.SeatBid) (*adapters.BidderResponse, []error) { + errs := []error{} + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seats)) + + for _, seatBid := range seats { + for bidId, bid := range seatBid.Bid { + var bidExt bidExt + if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse Bid[%d].Ext: %s", bidId, err.Error()), + }) + continue + } + + bidType, err := openrtb_ext.ParseBidType(bidExt.Prebid.Type) + if err != nil { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bid[%d].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got '%s'", bidId, bidExt.Prebid.Type), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[bidId], + BidType: bidType, + }) + } + } + + return bidResponse, errs +} diff --git a/adapters/bematterfull/bematterfulltest/exemplary/banner.json b/adapters/bematterfull/bematterfulltest/exemplary/banner.json new file mode 100644 index 00000000000..d90cc677c0f --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/exemplary/banner.json @@ -0,0 +1,279 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + }, + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=533faf0754cd43ceab591077781b909b&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=533faf0754cd43ceab591077781b909b&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/exemplary/native.json b/adapters/bematterfull/bematterfulltest/exemplary/native.json new file mode 100644 index 00000000000..7f81dce6237 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/exemplary/native.json @@ -0,0 +1,162 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=533faf0754cd43ceab591077781b909b&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/exemplary/video.json b/adapters/bematterfull/bematterfulltest/exemplary/video.json new file mode 100644 index 00000000000..eb6a02a4249 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/exemplary/video.json @@ -0,0 +1,202 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=533faf0754cd43ceab591077781b909b&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/bad-response.json b/adapters/bematterfull/bematterfulltest/supplemental/bad-response.json new file mode 100644 index 00000000000..bf459fce210 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/bad-response.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=533faf0754cd43ceab591077781b902&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/empty-mediatype.json b/adapters/bematterfull/bematterfulltest/supplemental/empty-mediatype.json new file mode 100644 index 00000000000..b408c7510a7 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/empty-mediatype.json @@ -0,0 +1,188 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=533faf0754cd43ceab591077781b902&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "some": "value" + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid[1].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got ''", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/empty-seatbid-0-bid.json b/adapters/bematterfull/bematterfulltest/supplemental/empty-seatbid-0-bid.json new file mode 100644 index 00000000000..94c7905832d --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/empty-seatbid-0-bid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/empty-seatbid.json b/adapters/bematterfull/bematterfulltest/supplemental/empty-seatbid.json new file mode 100644 index 00000000000..3a7fee5a102 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/empty-seatbid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "bidid": "359da97d0384d8a14767029c18fd840d", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Array SeatBid cannot be empty", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/invalid-ext-bidder-object.json b/adapters/bematterfull/bematterfulltest/supplemental/invalid-ext-bidder-object.json new file mode 100644 index 00000000000..67778fd4b8e --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/invalid-ext-bidder-object.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": [] + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize Bematterfull extension: json: cannot unmarshal array into Go value of type openrtb_ext.ExtBematterfull", + "comparison": "literal" + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/invalid-ext-object.json b/adapters/bematterfull/bematterfulltest/supplemental/invalid-ext-object.json new file mode 100644 index 00000000000..aa215eb3e34 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/invalid-ext-object.json @@ -0,0 +1,47 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": "" + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize bidder impression extension: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/invalid-mediatype.json b/adapters/bematterfull/bematterfulltest/supplemental/invalid-mediatype.json new file mode 100644 index 00000000000..138dd7bbd5d --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/invalid-mediatype.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "wrong" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid[1].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got 'wrong'", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/status-204.json b/adapters/bematterfull/bematterfulltest/supplemental/status-204.json new file mode 100644 index 00000000000..e5cf2f4096c --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/status-204.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/status-400.json b/adapters/bematterfull/bematterfulltest/supplemental/status-400.json new file mode 100644 index 00000000000..0a74f74f751 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/status-400.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 400, + "body": "The Key has a different ad format" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/status-503.json b/adapters/bematterfull/bematterfulltest/supplemental/status-503.json new file mode 100644 index 00000000000..71c8936e94e --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/status-503.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bidder Bematterfull is unavailable. Please contact the bidder support.", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bematterfull/bematterfulltest/supplemental/unexpected-status.json b/adapters/bematterfull/bematterfulltest/supplemental/unexpected-status.json new file mode 100644 index 00000000000..9f4c43ab0d2 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest/supplemental/unexpected-status.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.mtflll-system.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=mtflll-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "mtflll-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 403, + "body": "Access is denied" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bematterfull/bematterfulltest_test.go b/adapters/bematterfull/bematterfulltest_test.go new file mode 100644 index 00000000000..f3d1f412761 --- /dev/null +++ b/adapters/bematterfull/bematterfulltest_test.go @@ -0,0 +1,27 @@ +package bematterfull + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderSmartHub, + config.Adapter{ + Endpoint: "http://prebid-srv.mtflll-system.live/?pid={{.SourceId}}&host={{.Host}}", + }, + config.Server{ + ExternalUrl: "http://hosturl.com", + GvlID: 1, + DataCenter: "2", + }, + ) + + assert.NoError(t, buildErr) + adapterstest.RunJSONBidderTest(t, "bematterfulltest", bidder) +} diff --git a/adapters/bematterfull/params_test.go b/adapters/bematterfull/params_test.go new file mode 100644 index 00000000000..5983dd3e375 --- /dev/null +++ b/adapters/bematterfull/params_test.go @@ -0,0 +1,53 @@ +package bematterfull + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{"env":"mtflll-stage", "pid":"123456"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderBematterfull, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected bematterfull params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"some": "param"}`, + `{"env":"mtflll-stage"}`, + `{"pid":"1234"}`, + `{"othervalue":"Lorem ipsum"}`, + `{"env":"mtflll-stage", pid:""}`, + `{"env":"", pid:"1234"}`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderBematterfull, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/bidder_test.go b/adapters/bidder_test.go index f0b833cec2e..d6373bdeab1 100644 --- a/adapters/bidder_test.go +++ b/adapters/bidder_test.go @@ -52,12 +52,12 @@ type mockConversions struct { mock.Mock } -func (m mockConversions) GetRate(from string, to string) (float64, error) { +func (m *mockConversions) GetRate(from string, to string) (float64, error) { args := m.Called(from, to) return args.Get(0).(float64), args.Error(1) } -func (m mockConversions) GetRates() *map[string]map[string]float64 { +func (m *mockConversions) GetRates() *map[string]map[string]float64 { args := m.Called() return args.Get(0).(*map[string]map[string]float64) } diff --git a/adapters/bidscube/bidscubetest/supplemental/bad_bidtype_response.json b/adapters/bidscube/bidscubetest/supplemental/bad_bidtype_response.json index 62107dad25d..998ef2dd309 100644 --- a/adapters/bidscube/bidscubetest/supplemental/bad_bidtype_response.json +++ b/adapters/bidscube/bidscubetest/supplemental/bad_bidtype_response.json @@ -96,6 +96,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unable to read bid.ext.prebid.type: Key path not found", diff --git a/adapters/bidstack/bidstack.go b/adapters/bidstack/bidstack.go index a1a4e6ec7f6..c5c7f5b4865 100644 --- a/adapters/bidstack/bidstack.go +++ b/adapters/bidstack/bidstack.go @@ -37,7 +37,7 @@ func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) ( return bidder, nil } -func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { headers, err := prepareHeaders(request) if err != nil { return nil, []error{fmt.Errorf("headers prepare: %v", err)} diff --git a/adapters/bliink/bliinktest/supplemental/multiple_format_request.json b/adapters/bliink/bliinktest/supplemental/multiple_format_request.json index 43ee1e9dd4d..c85bc9ed822 100644 --- a/adapters/bliink/bliinktest/supplemental/multiple_format_request.json +++ b/adapters/bliink/bliinktest/supplemental/multiple_format_request.json @@ -116,10 +116,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unable to fetch mediaType in multi-format: some-impression-id", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/bluesea/bluesea.go b/adapters/bluesea/bluesea.go new file mode 100644 index 00000000000..44745ef00e8 --- /dev/null +++ b/adapters/bluesea/bluesea.go @@ -0,0 +1,161 @@ +package bluesea + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type blueseaBidExt struct { + MediaType string `json:"mediatype"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + impCount := len(request.Imp) + + if impCount == 0 { + err := &errortypes.BadInput{ + Message: "Empty Imp objects", + } + return nil, []error{err} + } + + requestDatas := make([]*adapters.RequestData, 0, impCount) + errs := make([]error, 0, impCount) + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + for _, imp := range request.Imp { + blueseaImpExt, err := extraImpExt(&imp) + if err != nil { + errs = append(errs, err) + continue + } + reqJson, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + continue + } + + queryParams := url.Values{} + queryParams.Add("pubid", blueseaImpExt.PubId) + queryParams.Add("token", blueseaImpExt.Token) + queryString := queryParams.Encode() + requestData := &adapters.RequestData{ + Method: "POST", + Uri: fmt.Sprintf("%s?%s", a.endpoint, queryString), + Body: reqJson, + Headers: headers, + } + requestDatas = append(requestDatas, requestData) + } + // to safe double check in case the requestDatas is empty and no error is raised. + if len(requestDatas) == 0 && len(errs) == 0 { + errs = append(errs, fmt.Errorf("Empty RequestData")) + } + return requestDatas, errs +} + +func extraImpExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpBluesea, error) { + var impExt adapters.ExtImpBidder + var blueseaImpExt openrtb_ext.ExtImpBluesea + + err := json.Unmarshal(imp.Ext, &impExt) + if err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Error in parsing imp.ext. err = %v, imp.ext = %v", err.Error(), string(imp.Ext)), + } + } + + err = json.Unmarshal(impExt.Bidder, &blueseaImpExt) + if err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Error in parsing imp.ext.bidder. err = %v, bidder = %v", err.Error(), string(impExt.Bidder)), + } + } + if len(blueseaImpExt.PubId) == 0 || len(blueseaImpExt.Token) == 0 { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Error in parsing imp.ext.bidder, empty pubid or token"), + } + } + return &blueseaImpExt, nil +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{err} + } + + var blueseaResponse openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &blueseaResponse); err != nil { + return nil, []error{fmt.Errorf("Error in parsing bidresponse body")} + } + + var errs []error + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + + if blueseaResponse.Cur != "" { + bidResponse.Currency = blueseaResponse.Cur + } + for _, seatBid := range blueseaResponse.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(&bid) + + if err != nil { + errs = append(errs, err) + continue + } + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, errs +} + +func getMediaTypeForBid(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { + + var bidExt blueseaBidExt + if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { + return "", fmt.Errorf("Error in parsing bid.ext") + } + + switch bidExt.MediaType { + case "banner": + return openrtb_ext.BidTypeBanner, nil + case "native": + return openrtb_ext.BidTypeNative, nil + case "video": + return openrtb_ext.BidTypeVideo, nil + default: + return "", fmt.Errorf("Unknown bid type, %v", bidExt.MediaType) + } +} diff --git a/adapters/bluesea/bluesea_test.go b/adapters/bluesea/bluesea_test.go new file mode 100644 index 00000000000..6b5ad16eb85 --- /dev/null +++ b/adapters/bluesea/bluesea_test.go @@ -0,0 +1,21 @@ +package bluesea + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderBluesea, config.Adapter{ + Endpoint: "https://test.prebid.bluesea"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "blueseatest", bidder) +} diff --git a/adapters/bluesea/blueseatest/exemplary/banner.json b/adapters/bluesea/blueseatest/exemplary/banner.json new file mode 100644 index 00000000000..5bb9b44670c --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/banner.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-crid", + "w":300, + "ext":{ + "mediatype":"banner" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-crid", + "w":300, + "ext":{ + "mediatype":"banner" + } + }, + "type":"banner" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/native.json b/adapters/bluesea/blueseatest/exemplary/native.json new file mode 100644 index 00000000000..c70523564a0 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/native.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "native":{ + "request":"{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":150}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":100,\"hmin\":100}},{\"id\":3,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver":"1.2" + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "native":{ + "request":"{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":150}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":100,\"hmin\":100}},{\"id\":3,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver":"1.2" + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-native-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-native-crid", + "w":300, + "ext":{ + "mediatype":"native" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-native-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-native-crid", + "w":300, + "ext":{ + "mediatype":"native" + } + }, + "type":"native" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/nobid.json b/adapters/bluesea/blueseatest/exemplary/nobid.json new file mode 100644 index 00000000000..4e881471818 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/nobid.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses":[ + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/video.json b/adapters/bluesea/blueseatest/exemplary/video.json new file mode 100644 index 00000000000..7092583aa54 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/video.json @@ -0,0 +1,162 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "video":{ + "mimes":[ + "video/mp4", + "application/javascript", + "video/webm" + ], + "minduration":5, + "maxduration":120, + "protocols":[ + 2, + 3, + 5, + 6 + ], + "pos":7, + "w":320, + "h":480, + "linearity":1 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "video":{ + "mimes":[ + "video/mp4", + "application/javascript", + "video/webm" + ], + "minduration":5, + "maxduration":120, + "protocols":[ + 2, + 3, + 5, + 6 + ], + "pos":7, + "w":320, + "h":480, + "linearity":1 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-vast", + "impid":"1", + "id":"test-bid-id", + "h":480, + "adomain":[ + "adv.com" + ], + "crid":"test-crid", + "w":320, + "ext":{ + "mediatype":"video" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-vast", + "impid":"1", + "id":"test-bid-id", + "h":480, + "adomain":[ + "adv.com" + ], + "crid":"test-crid", + "w":320, + "ext":{ + "mediatype":"video" + } + }, + "type":"video" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/supplemental/invalid-media-type.json b/adapters/bluesea/blueseatest/supplemental/invalid-media-type.json new file mode 100644 index 00000000000..b4b4ad25ae3 --- /dev/null +++ b/adapters/bluesea/blueseatest/supplemental/invalid-media-type.json @@ -0,0 +1,112 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-crid", + "w":300 + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "comparison": "literal", + "value": "Error in parsing bid.ext" + } + ] +} diff --git a/adapters/bluesea/blueseatest/supplemental/invalid-params.json b/adapters/bluesea/blueseatest/supplemental/invalid-params.json new file mode 100644 index 00000000000..603a2b91533 --- /dev/null +++ b/adapters/bluesea/blueseatest/supplemental/invalid-params.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "expectedMakeRequestsErrors":[ + { + "value":"Error in parsing imp.ext.bidder, empty pubid or token", + "comparison":"literal" + } + ] +} diff --git a/adapters/bluesea/blueseatest/supplemental/malformed-body-response.json b/adapters/bluesea/blueseatest/supplemental/malformed-body-response.json new file mode 100644 index 00000000000..7fef3f6d6e2 --- /dev/null +++ b/adapters/bluesea/blueseatest/supplemental/malformed-body-response.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status":200, + "body":"{" + } + } + ], + + "expectedMakeBidsErrors": [ + { + "comparison": "literal", + "value": "Error in parsing bidresponse body" + } + ] +} diff --git a/adapters/bluesea/blueseatest/supplemental/status-400-response.json b/adapters/bluesea/blueseatest/supplemental/status-400-response.json new file mode 100644 index 00000000000..6fcb6a21c7e --- /dev/null +++ b/adapters/bluesea/blueseatest/supplemental/status-400-response.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "app":{ + "name":"test-app", + "bundle":"com.test.app", + "ver":"10400" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720, + "ifa":"test-ifa" + }, + "at":1, + "tmax":1200, + "test":1 + } + }, + "mockResponse":{ + "status":400, + "body":"" + } + } + ], + + "expectedMakeBidsErrors": [ + { + "comparison": "literal", + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info" + } + ] +} diff --git a/adapters/bluesea/params_test.go b/adapters/bluesea/params_test.go new file mode 100644 index 00000000000..62f14ee8c57 --- /dev/null +++ b/adapters/bluesea/params_test.go @@ -0,0 +1,49 @@ +package bluesea + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TestValidParams makes sure that the bluesea schema accepts all imp.ext fields which we intend to support. +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderBluesea, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected bluesea params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the bluesea schema rejects all the imp.ext fields we don't support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderBluesea, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"pubid": "1001", "token": "t35w089r1n92k946"}`, +} + +var invalidParams = []string{ + `{1001`, + `{"pubid": "1001"}`, + `{"pubid": "1001", "Token": "invalid-token"}`, + `{"Pubid": "1001", "token": "invalid-token"}`, + `{"pubid": "abc", "token": "t35w089r1n92k946"}`, + `{"pubid": "1001", "token": "test-token"}`, +} diff --git a/adapters/brightroll/brightroll.go b/adapters/brightroll/brightroll.go deleted file mode 100644 index a917fe87277..00000000000 --- a/adapters/brightroll/brightroll.go +++ /dev/null @@ -1,263 +0,0 @@ -package brightroll - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - - "github.com/prebid/openrtb/v19/adcom1" - "github.com/prebid/openrtb/v19/openrtb2" - "github.com/prebid/prebid-server/adapters" - "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/errortypes" - "github.com/prebid/prebid-server/openrtb_ext" -) - -type BrightrollAdapter struct { - URI string - extraInfo ExtraInfo -} - -type ExtraInfo struct { - Accounts []Account `json:"accounts"` -} - -type Account struct { - ID string `json:"id"` - Badv []string `json:"badv"` - Bcat []string `json:"bcat"` - Battr []int8 `json:"battr"` - BidFloor float64 `json:"bidfloor"` -} - -func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - - request := *requestIn - errs := make([]error, 0, len(request.Imp)) - if len(request.Imp) == 0 { - err := &errortypes.BadInput{ - Message: "No impression in the bid request", - } - errs = append(errs, err) - return nil, errs - } - - errors := make([]error, 0, 1) - - var bidderExt adapters.ExtImpBidder - err := json.Unmarshal(request.Imp[0].Ext, &bidderExt) - if err != nil { - err = &errortypes.BadInput{ - Message: "ext.bidder not provided", - } - errors = append(errors, err) - return nil, errors - } - var brightrollExt openrtb_ext.ExtImpBrightroll - err = json.Unmarshal(bidderExt.Bidder, &brightrollExt) - if err != nil { - err = &errortypes.BadInput{ - Message: "ext.bidder.publisher not provided", - } - errors = append(errors, err) - return nil, errors - } - if brightrollExt.Publisher == "" { - err = &errortypes.BadInput{ - Message: "publisher is empty", - } - errors = append(errors, err) - return nil, errors - } - - var account *Account - for _, a := range a.extraInfo.Accounts { - if a.ID == brightrollExt.Publisher { - account = &a - break - } - } - - if account == nil { - err = &errortypes.BadInput{ - Message: "Invalid publisher", - } - errors = append(errors, err) - return nil, errors - } - - validImpExists := false - for i := 0; i < len(request.Imp); i++ { - //Brightroll supports only banner and video impressions as of now - if request.Imp[i].Banner != nil { - bannerCopy := *request.Imp[i].Banner - if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { - firstFormat := bannerCopy.Format[0] - bannerCopy.W = &(firstFormat.W) - bannerCopy.H = &(firstFormat.H) - } - - if len(account.Battr) > 0 { - bannerCopy.BAttr = getBlockedCreativetypes(account.Battr) - } - request.Imp[i].Banner = &bannerCopy - validImpExists = true - } else if request.Imp[i].Video != nil { - validImpExists = true - if brightrollExt.Publisher == "adthrive" { - videoCopy := *request.Imp[i].Video - if len(account.Battr) > 0 { - videoCopy.BAttr = getBlockedCreativetypes(account.Battr) - } - request.Imp[i].Video = &videoCopy - } - } - if validImpExists && request.Imp[i].BidFloor == 0 && account.BidFloor > 0 { - request.Imp[i].BidFloor = account.BidFloor - } - } - if !validImpExists { - err := &errortypes.BadInput{ - Message: fmt.Sprintf("No valid impression in the bid request"), - } - errs = append(errs, err) - return nil, errs - } - - request.AT = 1 //Defaulting to first price auction for all prebid requests - - if len(account.Bcat) > 0 { - request.BCat = account.Bcat - } - - if len(account.Badv) > 0 { - request.BAdv = account.Badv - } - reqJSON, err := json.Marshal(request) - if err != nil { - errs = append(errs, err) - return nil, errs - } - thisURI := a.URI - thisURI = thisURI + "?publisher=" + brightrollExt.Publisher - headers := http.Header{} - headers.Add("Content-Type", "application/json;charset=utf-8") - headers.Add("Accept", "application/json") - headers.Add("x-openrtb-version", "2.5") - - if request.Device != nil { - addHeaderIfNonEmpty(headers, "User-Agent", request.Device.UA) - addHeaderIfNonEmpty(headers, "X-Forwarded-For", request.Device.IP) - addHeaderIfNonEmpty(headers, "Accept-Language", request.Device.Language) - if request.Device.DNT != nil { - addHeaderIfNonEmpty(headers, "DNT", strconv.Itoa(int(*request.Device.DNT))) - } - } - - return []*adapters.RequestData{{ - Method: "POST", - Uri: thisURI, - Body: reqJSON, - Headers: headers, - }}, errors -} - -func (a *BrightrollAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { - - if response.StatusCode == http.StatusNoContent { - return nil, nil - } - - if response.StatusCode == http.StatusBadRequest { - return nil, []error{&errortypes.BadInput{ - Message: fmt.Sprintf("Unexpected status code: %d. ", response.StatusCode), - }} - } - - if response.StatusCode != http.StatusOK { - return nil, []error{&errortypes.BadServerResponse{ - Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), - }} - } - - var bidResp openrtb2.BidResponse - if err := json.Unmarshal(response.Body, &bidResp); err != nil { - return nil, []error{&errortypes.BadServerResponse{ - Message: fmt.Sprintf("bad server response: %d. ", err), - }} - } - - bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) - sb := bidResp.SeatBid[0] - for i := 0; i < len(sb.Bid); i++ { - bid := sb.Bid[i] - bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &bid, - BidType: getMediaTypeForImp(bid.ImpID, internalRequest.Imp), - }) - } - return bidResponse, nil -} - -func getBlockedCreativetypes(attr []int8) []adcom1.CreativeAttribute { - var creativeAttr []adcom1.CreativeAttribute - for i := 0; i < len(attr); i++ { - creativeAttr = append(creativeAttr, adcom1.CreativeAttribute(attr[i])) - } - return creativeAttr -} - -// Adding header fields to request header -func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) { - if len(headerValue) > 0 { - headers.Add(headerName, headerValue) - } -} - -// getMediaTypeForImp figures out which media type this bid is for. -func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType { - mediaType := openrtb_ext.BidTypeBanner //default type - for _, imp := range imps { - if imp.ID == impId { - if imp.Video != nil { - mediaType = openrtb_ext.BidTypeVideo - } - return mediaType - } - } - return mediaType -} - -// Builder builds a new instance of the Brightroll adapter for the given bidder with the given config. -func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { - extraInfo, err := getExtraInfo(config.ExtraAdapterInfo) - if err != nil { - return nil, err - } - - bidder := &BrightrollAdapter{ - URI: config.Endpoint, - extraInfo: extraInfo, - } - return bidder, nil -} - -func getExtraInfo(v string) (ExtraInfo, error) { - if len(v) == 0 { - return getDefaultExtraInfo(), nil - } - - var extraInfo ExtraInfo - if err := json.Unmarshal([]byte(v), &extraInfo); err != nil { - return extraInfo, fmt.Errorf("invalid extra info: %v", err) - } - - return extraInfo, nil -} - -func getDefaultExtraInfo() ExtraInfo { - return ExtraInfo{ - Accounts: []Account{}, - } -} diff --git a/adapters/brightroll/brightroll_test.go b/adapters/brightroll/brightroll_test.go deleted file mode 100644 index 4cf0f46fda7..00000000000 --- a/adapters/brightroll/brightroll_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package brightroll - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/prebid/prebid-server/adapters/adapterstest" - "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/openrtb_ext" -) - -func TestEmptyConfig(t *testing.T) { - bidder, buildErr := Builder(openrtb_ext.BidderBrightroll, config.Adapter{ - Endpoint: `http://test-bid.ybp.yahoo.com/bid/appnexuspbs`, - ExtraAdapterInfo: ``, - }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) - - if buildErr != nil { - t.Fatalf("Builder returned unexpected error %v", buildErr) - } - - ex := ExtraInfo{ - Accounts: []Account{}, - } - expected := &BrightrollAdapter{ - URI: "http://test-bid.ybp.yahoo.com/bid/appnexuspbs", - extraInfo: ex, - } - assert.Equal(t, expected, bidder) -} - -func TestNonEmptyConfig(t *testing.T) { - bidder, buildErr := Builder(openrtb_ext.BidderBrightroll, config.Adapter{ - Endpoint: `http://test-bid.ybp.yahoo.com/bid/appnexuspbs`, - ExtraAdapterInfo: `{"accounts": [{"id": "test","bidfloor":0.1}]}`, - }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) - - if buildErr != nil { - t.Fatalf("Builder returned unexpected error %v", buildErr) - } - - ex := ExtraInfo{ - Accounts: []Account{{ID: "test", BidFloor: 0.1}}, - } - expected := &BrightrollAdapter{ - URI: "http://test-bid.ybp.yahoo.com/bid/appnexuspbs", - extraInfo: ex, - } - assert.Equal(t, expected, bidder) -} - -func TestMalformedEmpty(t *testing.T) { - _, buildErr := Builder(openrtb_ext.BidderBrightroll, config.Adapter{ - Endpoint: `http://test-bid.ybp.yahoo.com/bid/appnexuspbs`, - ExtraAdapterInfo: `malformed`, - }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) - - assert.Error(t, buildErr) -} - -func TestJsonSamples(t *testing.T) { - bidder, buildErr := Builder(openrtb_ext.BidderBrightroll, config.Adapter{ - Endpoint: `http://test-bid.ybp.yahoo.com/bid/appnexuspbs`, - ExtraAdapterInfo: `{"accounts": [{"id": "adthrive","badv": [], "bcat": ["IAB8-5","IAB8-18"],"battr": [1,2,3], "bidfloor":0.0}]}`, - }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) - - if buildErr != nil { - t.Fatalf("Builder returned unexpected error %v", buildErr) - } - - adapterstest.RunJSONBidderTest(t, "brightrolltest", bidder) -} diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json b/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json deleted file mode 100644 index f67fa259c6d..00000000000 --- a/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-native-imp", - "native": { - "ver": "1.1", - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}" - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-audio-imp", - "audio": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - }, - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", - "body": { - "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], - "imp": [ - { - "id": "test-imp-id", - "banner": { - "battr": [ - 1, - 2, - 3 - ], - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ], - "w": 300, - "h": 250 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-native-imp", - "native": { - "ver": "1.1", - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}" - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-audio-imp", - "audio": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [{ - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300 - }] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - } - ], - - "expectedBidResponses": [ - { - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250 - }, - "type": "banner" - } - ] - } - ] - -} diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json b/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json deleted file mode 100644 index 97081b708d3..00000000000 --- a/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-native-imp", - "native": { - "ver": "1.1", - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}" - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "test-imp-video-id", - "video": { - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - }, - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", - "body": { - "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], - "imp": [ - { - "id": "test-imp-id", - "banner": { - "battr": [ - 1, - 2, - 3 - ], - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ], - "w": 300, - "h": 250 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-native-imp", - "native": { - "ver": "1.1", - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}" - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "test-imp-video-id", - "video": { - "battr": [ - 1, - 2, - 3 - ], - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [{ - "id": "7706636740145184841", - "impid": "test-imp-video-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "h": 576, - "w": 1024 - }] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - } - ], - - "expectedBidResponses": [ - { - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-video-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "w": 1024, - "h": 576 - }, - "type": "video" - } - ] - } - ] - -} diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-video.json b/adapters/brightroll/brightrolltest/exemplary/banner-video.json deleted file mode 100644 index 17441152edc..00000000000 --- a/adapters/brightroll/brightrolltest/exemplary/banner-video.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "test-imp-video-id", - "video": { - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - }, - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", - "body": { - "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], - "imp": [ - { - "id": "test-imp-id", - "banner": { - "battr": [ - 1, - 2, - 3 - ], - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ], - "w": 300, - "h": 250 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "test-imp-video-id", - "video": { - "battr": [ - 1, - 2, - 3 - ], - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [{ - "id": "7706636740145184841", - "impid": "test-imp-video-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "h": 576, - "w": 1024 - }] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - } - ], - - "expectedBidResponses": [ - { - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-video-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["yahoo.com"], - "cid": "958", - "crid": "29681110", - "w": 1024, - "h": 576 - }, - "type": "video" - } - ] - } - ] - -} diff --git a/adapters/brightroll/brightrolltest/exemplary/simple-video.json b/adapters/brightroll/brightrolltest/exemplary/simple-video.json deleted file mode 100644 index 88539276043..00000000000 --- a/adapters/brightroll/brightrolltest/exemplary/simple-video.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext":{ - "bidder":{ - "publisher": "adthrive" - } - } - } - ] - }, - - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", - "body": { - "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], - "imp": [ - { - "id": "test-imp-id", - "video": { - "battr": [ - 1, - 2, - 3 - ], - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "cur": "USD", - "seatbid": [ - { - "seat": "Brightroll", - "bid": [{ - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-id", - "price": 0.500000, - "adm": "some-test-ad", - "crid": "crid_10", - "w": 1024, - "h": 576 - }] - } - ] - } - } - } - ], - - "expectedBidResponses": [ - { - "bids": [ - { - "bid": { - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "crid": "crid_10", - "w": 1024, - "h": 576 - }, - "type": "video" - } - ] - } - ] -} diff --git a/adapters/brightroll/brightrolltest/exemplary/valid-extension.json b/adapters/brightroll/brightrolltest/exemplary/valid-extension.json deleted file mode 100644 index 9a5e571ce1b..00000000000 --- a/adapters/brightroll/brightrolltest/exemplary/valid-extension.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext":{ - "bidder":{ - "publisher": "adthrive" - } - } - } - ] - }, - - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", - "body": { - "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], - "imp": [ - { - "id": "test-imp-id", - "video": { - "battr": [ - 1, - 2, - 3 - ], - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "cur": "USD", - "seatbid": [ - { - "seat": "brightroll", - "bid": [{ - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-id", - "price": 0.500000, - "adm": "some-test-ad", - "crid": "crid_10", - "w": 1024, - "h": 576 - }] - } - ] - } - } - } - ], - - "expectedBidResponses": [ - { - "bids": [{ - "bid": { - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "crid": "crid_10", - "w": 1024, - "h": 576 - }, - "type": "video" - }] - } - ] -} diff --git a/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json b/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json deleted file mode 100644 index 4184842b60e..00000000000 --- a/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-video-id", - "video": { - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-audio-imp", - "audio": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - }, - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", - "body": { - "id": "test-request-id", - "at":1, - "bcat": [ - "IAB8-5", - "IAB8-18" - ], - "imp": [ - { - "id": "test-imp-video-id", - "video": { - "battr": [ - 1, - 2, - 3 - ], - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - { - "id": "unsupported-audio-imp", - "audio": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "cur": "USD", - "seatbid": [ - { - "seat": "brightroll", - "bid": [{ - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-video-id", - "price": 0.500000, - "adm": "some-test-ad", - "crid": "crid_10", - "w": 1024, - "h": 576 - }] - } - ] - } - } - } - ], - - "expectedBidResponses": [ - { - "bids": [{ - "bid": { - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-video-id", - "price": 0.5, - "adm": "some-test-ad", - "crid": "crid_10", - "w": 1024, - "h": 576 - }, - "type": "video" - }] - } - ] -} diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json b/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json deleted file mode 100644 index 01beec712c7..00000000000 --- a/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "ext": { - "bidder": { - "publisher": "adthrive" - } - } - }, - "expectedMakeRequestsErrors": [ - { - "value": "No impression in the bid request", - "comparison": "literal" - } - ] -} diff --git a/adapters/brightroll/brightrolltest/supplemental/missing-extension.json b/adapters/brightroll/brightrolltest/supplemental/missing-extension.json deleted file mode 100644 index 82ec775da30..00000000000 --- a/adapters/brightroll/brightrolltest/supplemental/missing-extension.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-missing-ext-id", - "video": { - "mimes": ["video/mp4"], - "protocols": [2, 5], - "w": 1024, - "h": 576 - } - } - ] - }, - "expectedMakeRequestsErrors": [ - { - "value": "ext.bidder not provided", - "comparison": "literal" - } - ] -} \ No newline at end of file diff --git a/adapters/brightroll/params_test.go b/adapters/brightroll/params_test.go deleted file mode 100644 index c14ee6d73ff..00000000000 --- a/adapters/brightroll/params_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package brightroll - -import ( - "encoding/json" - "testing" - - "github.com/prebid/prebid-server/openrtb_ext" -) - -// This file actually intends to test static/bidder-params/brightroll.json -// -// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.brightroll - -// TestValidParams makes sure that the Brightroll schema accepts all imp.ext fields which we intend to support. -func TestValidParams(t *testing.T) { - validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") - if err != nil { - t.Fatalf("Failed to fetch the json-schemas. %v", err) - } - - for _, validParam := range validParams { - if err := validator.Validate(openrtb_ext.BidderBrightroll, json.RawMessage(validParam)); err != nil { - t.Errorf("Schema rejected Brightroll params: %s", validParam) - } - } -} - -// TestInvalidParams makes sure that the Brightroll schema rejects all the imp.ext fields we don't support. -func TestInvalidParams(t *testing.T) { - validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") - if err != nil { - t.Fatalf("Failed to fetch the json-schemas. %v", err) - } - - for _, invalidParam := range invalidParams { - if err := validator.Validate(openrtb_ext.BidderBrightroll, json.RawMessage(invalidParam)); err == nil { - t.Errorf("Schema allowed unexpected params: %s", invalidParam) - } - } -} - -var validParams = []string{ - `{"publisher": "testpublisher"}`, - `{"publisher": "123"}`, - `{"publisher": "cafemedia"}`, - `{"publisher": "test", "headerbidding": false}`, -} - -var invalidParams = []string{ - `{"publisher": 100}`, - `{"headerbidding": false}`, - `{"publisher": true}`, - `{"publisherId": 123, "headerbidding": true}`, - `{"publisherID": "1"}`, - ``, - `null`, - `true`, - `9`, - `1.2`, - `[]`, - `{}`, -} diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-app.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-and-video-app.json similarity index 69% rename from adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-app.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-and-video-app.json index 46c469b9d28..255174e2ec1 100644 --- a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-app.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-and-video-app.json @@ -133,7 +133,7 @@ { "seat": "12356", "bid": [{ - "adm": "
", + "adm": "
", "id": "some_test_ad_id_1", "impid": "some_test_ad_id_1", "ttl": 300, @@ -168,7 +168,7 @@ { "bids": [{ "bid": { - "adm": "
", + "adm": "
", "id": "some_test_ad_id_1", "impid": "some_test_ad_id_1", "crid": "94395500", diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-and-video-site.json similarity index 69% rename from adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-and-video-site.json index f47fd66784e..1ea0397fced 100644 --- a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-and-video-site.json @@ -136,7 +136,7 @@ "seatbid": [{ "seat": "12356", "bid": [{ - "adm": "
", + "adm": "
", "id": "some_test_ad_id_1", "impid": "some_test_ad_id_1", "ttl": 300, @@ -170,7 +170,7 @@ "expectedBidResponses": [{ "bids": [{ "bid": { - "adm": "
", + "adm": "
", "id": "some_test_ad_id_1", "impid": "some_test_ad_id_1", "crid": "94395500", diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-app.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/banner-app.json diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/minimal-banner.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/minimal-banner.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/exemplary/minimal-banner.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/minimal-banner.json diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/video-app.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/video-app.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/exemplary/video-app.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/video-app.json diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/video-ctv.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/video-ctv.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/exemplary/video-ctv.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/video-ctv.json diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/video-site.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/video-site.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/exemplary/video-site.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/exemplary/video-site.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/add-bidfloor.json similarity index 95% rename from adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/add-bidfloor.json index 50ab8f16efc..2e3dbc3650f 100644 --- a/adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/add-bidfloor.json @@ -46,5 +46,5 @@ } } }], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/app-domain-and-url-correctly-parsed.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/app-domain-and-url-correctly-parsed.json similarity index 96% rename from adapters/emx_digital/emx_digitaltest/supplemental/app-domain-and-url-correctly-parsed.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/app-domain-and-url-correctly-parsed.json index 9e8856666e0..8930efaf470 100644 --- a/adapters/emx_digital/emx_digitaltest/supplemental/app-domain-and-url-correctly-parsed.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/app-domain-and-url-correctly-parsed.json @@ -63,5 +63,5 @@ } } ], - "expectedBidResponses":[] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/app-storeUrl-correctly-parsed.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/app-storeUrl-correctly-parsed.json similarity index 96% rename from adapters/emx_digital/emx_digitaltest/supplemental/app-storeUrl-correctly-parsed.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/app-storeUrl-correctly-parsed.json index 198adb5a008..4693a105c26 100644 --- a/adapters/emx_digital/emx_digitaltest/supplemental/app-storeUrl-correctly-parsed.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/app-storeUrl-correctly-parsed.json @@ -61,5 +61,5 @@ } } ], - "expectedBidResponses":[] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-banner-missing-sizes.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-banner-missing-sizes.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-banner-missing-sizes.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-banner-missing-sizes.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-ext-tagid-value.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-ext-tagid-value.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-ext-tagid-value.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-ext-tagid-value.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-mimes.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-video-missing-mimes.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-mimes.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-video-missing-mimes.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-sizes.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-video-missing-sizes.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-sizes.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/bad-imp-video-missing-sizes.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/build-banner-object.json similarity index 96% rename from adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/build-banner-object.json index f35e882be01..0c3d0d9aee3 100644 --- a/adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/build-banner-object.json @@ -65,5 +65,5 @@ } } }], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/build-video-object.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/build-video-object.json similarity index 96% rename from adapters/emx_digital/emx_digitaltest/supplemental/build-video-object.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/build-video-object.json index 235367c46a4..7076501da23 100644 --- a/adapters/emx_digital/emx_digitaltest/supplemental/build-video-object.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/build-video-object.json @@ -66,5 +66,5 @@ } } }], - "expectedBidResponses":[] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-request-no-banner.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/invalid-request-no-banner.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/invalid-request-no-banner.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/invalid-request-no-banner.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-no-bids.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/invalid-response-no-bids.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-no-bids.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/invalid-response-no-bids.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-unmarshall-error.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/invalid-response-unmarshall-error.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-unmarshall-error.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/invalid-response-unmarshall-error.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/no-imps-in-request.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/no-imps-in-request.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/no-imps-in-request.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/no-imps-in-request.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/server-error-code.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/server-error-code.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/server-error-code.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/server-error-code.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/server-no-content.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/server-no-content.json similarity index 100% rename from adapters/emx_digital/emx_digitaltest/supplemental/server-no-content.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/server-no-content.json diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/site-domain-and-url-correctly-parsed.json similarity index 96% rename from adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json rename to adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/site-domain-and-url-correctly-parsed.json index 0d71ade6e86..22160cf319b 100644 --- a/adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json +++ b/adapters/cadent_aperture_mx/cadent_aperture_mxtest/supplemental/site-domain-and-url-correctly-parsed.json @@ -63,5 +63,5 @@ } } ], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/emx_digital/emx_digital.go b/adapters/cadent_aperture_mx/cadentaperturemx.go similarity index 84% rename from adapters/emx_digital/emx_digital.go rename to adapters/cadent_aperture_mx/cadentaperturemx.go index 1b38263c6ff..5df000bf547 100644 --- a/adapters/emx_digital/emx_digital.go +++ b/adapters/cadent_aperture_mx/cadentaperturemx.go @@ -1,4 +1,4 @@ -package emx_digital +package cadentaperturemx import ( "encoding/json" @@ -17,7 +17,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -type EmxDigitalAdapter struct { +type adapter struct { endpoint string testing bool } @@ -33,7 +33,7 @@ func buildEndpoint(endpoint string, testing bool, timeout int64) string { return endpoint + "?t=" + strconv.FormatInt(timeout, 10) + "&ts=" + strconv.FormatInt(time.Now().Unix(), 10) + "&src=pbserver" } -func (a *EmxDigitalAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var errs []error if len(request.Imp) == 0 { @@ -81,7 +81,7 @@ func (a *EmxDigitalAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo * }}, errs } -func unpackImpExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpEmxDigital, error) { +func unpackImpExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpCadentApertureMX, error) { var bidderExt adapters.ExtImpBidder if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { return nil, &errortypes.BadInput{ @@ -89,27 +89,27 @@ func unpackImpExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpEmxDigital, error) { } } - var emxExt openrtb_ext.ExtImpEmxDigital - if err := json.Unmarshal(bidderExt.Bidder, &emxExt); err != nil { + var cadentExt openrtb_ext.ExtImpCadentApertureMX + if err := json.Unmarshal(bidderExt.Bidder, &cadentExt); err != nil { return nil, &errortypes.BadInput{ Message: fmt.Sprintf("ignoring imp id=%s, invalid ImpExt", imp.ID), } } - tagIDValidation, err := strconv.ParseInt(emxExt.TagID, 10, 64) + tagIDValidation, err := strconv.ParseInt(cadentExt.TagID, 10, 64) if err != nil || tagIDValidation == 0 { return nil, &errortypes.BadInput{ Message: fmt.Sprintf("ignoring imp id=%s, invalid tagid must be a String of numbers", imp.ID), } } - if emxExt.TagID == "" { + if cadentExt.TagID == "" { return nil, &errortypes.BadInput{ Message: fmt.Sprintf("Ignoring imp id=%s, no tagid present", imp.ID), } } - return &emxExt, nil + return &cadentExt, nil } func buildImpBanner(imp *openrtb2.Imp) error { @@ -175,13 +175,13 @@ func cleanProtocol(protocols []adcom1.MediaCreativeSubtype) []adcom1.MediaCreati return newitems } -// Add EMX required properties to Imp object -func addImpProps(imp *openrtb2.Imp, secure *int8, emxExt *openrtb_ext.ExtImpEmxDigital) { - imp.TagID = emxExt.TagID +// Add Cadent required properties to Imp object +func addImpProps(imp *openrtb2.Imp, secure *int8, cadentExt *openrtb_ext.ExtImpCadentApertureMX) { + imp.TagID = cadentExt.TagID imp.Secure = secure - if emxExt.BidFloor != "" { - bidFloor, err := strconv.ParseFloat(emxExt.BidFloor, 64) + if cadentExt.BidFloor != "" { + bidFloor, err := strconv.ParseFloat(cadentExt.BidFloor, 64) if err != nil { bidFloor = 0 } @@ -202,7 +202,7 @@ func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue str } } -// Handle request errors and formatting to be sent to EMX +// Handle request errors and formatting to be sent to Cadent func preprocess(request *openrtb2.BidRequest) []error { impsCount := len(request.Imp) errors := make([]error, 0, impsCount) @@ -225,13 +225,13 @@ func preprocess(request *openrtb2.BidRequest) []error { } for _, imp := range request.Imp { - emxExt, err := unpackImpExt(&imp) + cadentExt, err := unpackImpExt(&imp) if err != nil { errors = append(errors, err) continue } - addImpProps(&imp, &secure, emxExt) + addImpProps(&imp, &secure, cadentExt) if imp.Video != nil { if err := buildImpVideo(&imp); err != nil { @@ -253,7 +253,7 @@ func preprocess(request *openrtb2.BidRequest) []error { } // MakeBids make the bids for the bid response. -func (a *EmxDigitalAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { // no bid response @@ -309,9 +309,9 @@ func ContainsAny(raw string, keys []string) bool { } -// Builder builds a new instance of the EmxDigital adapter for the given bidder with the given config. +// Builder builds a new instance of the Cadent Aperture MX adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { - bidder := &EmxDigitalAdapter{ + bidder := &adapter{ endpoint: config.Endpoint, testing: false, } diff --git a/adapters/emx_digital/emx_digital_test.go b/adapters/cadent_aperture_mx/cadentaperturemx_test.go similarity index 66% rename from adapters/emx_digital/emx_digital_test.go rename to adapters/cadent_aperture_mx/cadentaperturemx_test.go index 6538ff70efa..60034322797 100644 --- a/adapters/emx_digital/emx_digital_test.go +++ b/adapters/cadent_aperture_mx/cadentaperturemx_test.go @@ -1,4 +1,4 @@ -package emx_digital +package cadentaperturemx import ( "testing" @@ -10,7 +10,7 @@ import ( ) func TestJsonSamples(t *testing.T) { - bidder, buildErr := Builder(openrtb_ext.BidderEmxDigital, config.Adapter{ + bidder, buildErr := Builder(openrtb_ext.BidderCadentApertureMX, config.Adapter{ Endpoint: "https://hb.emxdgt.com"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { @@ -18,10 +18,10 @@ func TestJsonSamples(t *testing.T) { } setTesting(bidder) - adapterstest.RunJSONBidderTest(t, "emx_digitaltest", bidder) + adapterstest.RunJSONBidderTest(t, "cadent_aperture_mxtest", bidder) } func setTesting(bidder adapters.Bidder) { - bidderEmxDigital, _ := bidder.(*EmxDigitalAdapter) - bidderEmxDigital.testing = true + bidderCadentApertureMX, _ := bidder.(*adapter) + bidderCadentApertureMX.testing = true } diff --git a/adapters/emx_digital/params_test.go b/adapters/cadent_aperture_mx/params_test.go similarity index 75% rename from adapters/emx_digital/params_test.go rename to adapters/cadent_aperture_mx/params_test.go index 49ad9eb1a9c..b5b7355376f 100644 --- a/adapters/emx_digital/params_test.go +++ b/adapters/cadent_aperture_mx/params_test.go @@ -1,4 +1,4 @@ -package emx_digital +package cadentaperturemx import ( "encoding/json" @@ -14,8 +14,8 @@ func TestValidParams(t *testing.T) { } for _, validParam := range validParams { - if err := validator.Validate(openrtb_ext.BidderEmxDigital, json.RawMessage(validParam)); err != nil { - t.Errorf("Schema rejected emx_digital params: %s", validParam) + if err := validator.Validate(openrtb_ext.BidderCadentApertureMX, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected cadent_aperture_mx params: %s", validParam) } } } @@ -27,7 +27,7 @@ func TestInvalidParams(t *testing.T) { } for _, invalidParam := range invalidParams { - if err := validator.Validate(openrtb_ext.BidderEmxDigital, json.RawMessage(invalidParam)); err == nil { + if err := validator.Validate(openrtb_ext.BidderCadentApertureMX, json.RawMessage(invalidParam)); err == nil { t.Errorf("Schema allowed unexpected params: %s", invalidParam) } } diff --git a/adapters/colossus/colossustest/supplemental/bad_media_type.json b/adapters/colossus/colossustest/supplemental/bad_media_type.json index e4a21787b3a..828e0a807a5 100644 --- a/adapters/colossus/colossustest/supplemental/bad_media_type.json +++ b/adapters/colossus/colossustest/supplemental/bad_media_type.json @@ -72,6 +72,7 @@ } } }], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to find impression \"test-imp-id\"", diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go index 9969ea3796e..0e16497774c 100644 --- a/adapters/consumable/consumable.go +++ b/adapters/consumable/consumable.go @@ -40,6 +40,8 @@ type bidRequest struct { Coppa bool `json:"coppa,omitempty"` SChain openrtb2.SupplyChain `json:"schain"` Content *openrtb2.Content `json:"content,omitempty"` + GPP string `json:"gpp,omitempty"` + GPPSID []int8 `json:"gpp_sid,omitempty"` } type placement struct { @@ -192,6 +194,14 @@ func (a *ConsumableAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo * body.Coppa = request.Regs != nil && request.Regs.COPPA > 0 + if request.Regs != nil && request.Regs.GPP != "" { + body.GPP = request.Regs.GPP + } + + if request.Regs != nil && request.Regs.GPPSID != nil { + body.GPPSID = request.Regs.GPPSID + } + if request.Site != nil && request.Site.Content != nil { body.Content = request.Site.Content } else if request.App != nil && request.App.Content != nil { diff --git a/adapters/consumable/consumable/supplemental/simple-banner-gpp.json b/adapters/consumable/consumable/supplemental/simple-banner-gpp.json new file mode 100644 index 00000000000..9b2e3d75031 --- /dev/null +++ b/adapters/consumable/consumable/supplemental/simple-banner-gpp.json @@ -0,0 +1,130 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 250}] + }, + "ext": { + "bidder": { + "networkId": 11, + "siteId": 32, + "unitId": 42 + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "123.123.123.123" + }, + "site": { + "domain": "www.some.com", + "page": "http://www.some.com/page-where-ad-will-be-shown" + }, + "regs": { + "gpp": "gppString", + "gpp_sid": [7] + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://e.serverbid.com/api/v2", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Forwarded": [ + "for=123.123.123.123" + ], + "Origin": [ + "http://www.some.com" + ], + "Referer": [ + "http://www.some.com/page-where-ad-will-be-shown" + ] + }, + "body": { + "placements": [ + { + "adTypes": [2730], + "divName": "test-imp-id", + "networkId": 11, + "siteId": 32, + "unitId": 42 + } + ], + "schain": { + "complete": 0, + "nodes": null, + "ver": "" + }, + "networkId": 11, + "siteId": 32, + "unitId": 42, + "time": 1451651415, + "url": "http://www.some.com/page-where-ad-will-be-shown", + "includePricingData": true, + "user":{}, + "enableBotFiltering": true, + "parallel": true, + "gpp": "gppString", + "gpp_sid": [7] + } + }, + "mockResponse": { + "status": 200, + "body": { + "decisions": { + "test-imp-id": { + "adId": 1234567890, + "pricing": { + "clearPrice": 0.5 + }, + "width": 728, + "height": 250, + "impressionUrl": "http://localhost:8080/shown", + "contents" : [ + { + "body": "Remember this: https://www.google.com/search?q=blink+tag ?" + } + ] + } + } + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "Remember this: https://www.google.com/search?q=blink+tag ?", + "crid": "1234567890", + "exp": 30, + "w": 728, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/conversant/conversant.go b/adapters/conversant/conversant.go index 55e6e2d14b4..ec4d2078df8 100644 --- a/adapters/conversant/conversant.go +++ b/adapters/conversant/conversant.go @@ -17,7 +17,7 @@ type ConversantAdapter struct { URI string } -func (c ConversantAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { +func (c *ConversantAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { for i := 0; i < len(request.Imp); i++ { var bidderExt adapters.ExtImpBidder if err := json.Unmarshal(request.Imp[i].Ext, &bidderExt); err != nil { @@ -132,7 +132,7 @@ func parseCnvrParams(imp *openrtb2.Imp, cnvrExt openrtb_ext.ExtImpConversant) { } } -func (c ConversantAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (c *ConversantAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { return nil, nil // no bid response } diff --git a/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json b/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json index 6ab02db0ca7..e1e5ddf9dba 100644 --- a/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json +++ b/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json @@ -68,10 +68,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "bid id='test-bid-id' could not find valid impid='BOGUS-IMPID'", "comparison": "regex" } ] -} \ No newline at end of file +} diff --git a/adapters/criteo/criteo.go b/adapters/criteo/criteo.go index ac96574c57d..11d8d166ba6 100644 --- a/adapters/criteo/criteo.go +++ b/adapters/criteo/criteo.go @@ -16,6 +16,15 @@ type adapter struct { endpoint string } +type BidExt struct { + Prebid ExtPrebid `json:"prebid"` +} + +type ExtPrebid struct { + BidType openrtb_ext.BidType `json:"type"` + NetworkName string `json:"networkName"` +} + func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &adapter{ endpoint: config.Endpoint, @@ -67,7 +76,7 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R for _, seatBid := range response.SeatBid { for i := range seatBid.Bid { - var bidExt openrtb_ext.ExtBid + var bidExt BidExt if err := json.Unmarshal(seatBid.Bid[i].Ext, &bidExt); err != nil { return nil, []error{&errortypes.BadServerResponse{ Message: fmt.Sprintf("Missing ext.prebid.type in bid for impression : %s.", seatBid.Bid[i].ImpID), @@ -76,7 +85,8 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R b := &adapters.TypedBid{ Bid: &seatBid.Bid[i], - BidType: bidExt.Prebid.Type, + BidType: bidExt.Prebid.BidType, + BidMeta: getBidMeta(bidExt), } bidResponse.Bids = append(bidResponse.Bids, b) } @@ -84,3 +94,13 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R return bidResponse, nil } + +func getBidMeta(ext BidExt) *openrtb_ext.ExtBidPrebidMeta { + var bidMeta *openrtb_ext.ExtBidPrebidMeta + if ext.Prebid.NetworkName != "" { + bidMeta = &openrtb_ext.ExtBidPrebidMeta{ + NetworkName: ext.Prebid.NetworkName, + } + } + return bidMeta +} diff --git a/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json b/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json index 38ed5c450b9..af62c3d1a6c 100755 --- a/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json +++ b/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json @@ -84,7 +84,8 @@ "h": 250, "ext": { "prebid": { - "type": "banner" + "type": "banner", + "networkName": "Criteo" } } } @@ -110,7 +111,8 @@ "h": 250, "ext": { "prebid": { - "type": "banner" + "type": "banner", + "networkName": "Criteo" } } }, diff --git a/adapters/datablocks/datablocks.go b/adapters/datablocks/datablocks.go index 8929c84853a..a39721a44a3 100644 --- a/adapters/datablocks/datablocks.go +++ b/adapters/datablocks/datablocks.go @@ -27,7 +27,7 @@ func (a *DatablocksAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo * "Accept": {"application/json"}, } - // Pull the host and source ID info from the bidder params. + // Pull the source ID info from the bidder params. reqImps, err := splitImpressions(request.Imp) if err != nil { @@ -45,7 +45,7 @@ func (a *DatablocksAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo * continue } - urlParams := macros.EndpointTemplateParams{Host: reqExt.Host, SourceId: strconv.Itoa(reqExt.SourceId)} + urlParams := macros.EndpointTemplateParams{SourceId: strconv.Itoa(reqExt.SourceId)} url, err := macros.ResolveMacros(a.EndpointTemplate, urlParams) if err != nil { @@ -137,7 +137,7 @@ func getBidderParams(imp *openrtb2.Imp) (*openrtb_ext.ExtImpDatablocks, error) { var datablocksExt openrtb_ext.ExtImpDatablocks if err := json.Unmarshal(bidderExt.Bidder, &datablocksExt); err != nil { return nil, &errortypes.BadInput{ - Message: fmt.Sprintf("Cannot Resolve host or sourceId: %s", err.Error()), + Message: fmt.Sprintf("Cannot Resolve sourceId: %s", err.Error()), } } @@ -147,12 +147,6 @@ func getBidderParams(imp *openrtb2.Imp) (*openrtb_ext.ExtImpDatablocks, error) { } } - if len(datablocksExt.Host) < 1 { - return nil, &errortypes.BadInput{ - Message: "Invalid/Missing Host", - } - } - return &datablocksExt, nil } diff --git a/adapters/datablocks/datablocks_test.go b/adapters/datablocks/datablocks_test.go index 9a5fb9a23f4..3d4d746d357 100644 --- a/adapters/datablocks/datablocks_test.go +++ b/adapters/datablocks/datablocks_test.go @@ -11,7 +11,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderDatablocks, config.Adapter{ - Endpoint: "http://{{.Host}}/openrtb2?sid={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "http://pbserver.dblks.net/openrtb2?sid={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { t.Fatalf("Builder returned unexpected error %v", buildErr) diff --git a/adapters/datablocks/datablockstest/exemplary/multi-request.json b/adapters/datablocks/datablockstest/exemplary/multi-request.json index 286e058a72e..eafd80927fa 100644 --- a/adapters/datablocks/datablockstest/exemplary/multi-request.json +++ b/adapters/datablocks/datablockstest/exemplary/multi-request.json @@ -17,7 +17,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } @@ -34,7 +34,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } @@ -54,7 +54,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=906295", + "uri": "http://pbserver.dblks.net/openrtb2?sid=906295", "body": { "id": "some-request-id", @@ -73,7 +73,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } @@ -91,7 +91,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } diff --git a/adapters/datablocks/datablockstest/exemplary/native.json b/adapters/datablocks/datablockstest/exemplary/native.json index 9487079c734..c4cf189e1b4 100644 --- a/adapters/datablocks/datablockstest/exemplary/native.json +++ b/adapters/datablocks/datablockstest/exemplary/native.json @@ -14,7 +14,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } @@ -38,7 +38,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=906295", + "uri": "http://pbserver.dblks.net/openrtb2?sid=906295", "body": { "id": "some-request-id", @@ -54,7 +54,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } diff --git a/adapters/datablocks/datablockstest/exemplary/simple-banner.json b/adapters/datablocks/datablockstest/exemplary/simple-banner.json index 289549ab588..5c01a29a519 100644 --- a/adapters/datablocks/datablockstest/exemplary/simple-banner.json +++ b/adapters/datablocks/datablockstest/exemplary/simple-banner.json @@ -21,7 +21,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } @@ -41,7 +41,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=906295", + "uri": "http://pbserver.dblks.net/openrtb2?sid=906295", "body": { "id": "some-request-id", @@ -64,7 +64,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } diff --git a/adapters/datablocks/datablockstest/exemplary/simple-video.json b/adapters/datablocks/datablockstest/exemplary/simple-video.json index 6f8073107f3..fcb2defa649 100644 --- a/adapters/datablocks/datablockstest/exemplary/simple-video.json +++ b/adapters/datablocks/datablockstest/exemplary/simple-video.json @@ -18,7 +18,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } @@ -39,7 +39,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=906295", + "uri": "http://pbserver.dblks.net/openrtb2?sid=906295", "body": { "id": "some-request-id", @@ -59,7 +59,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 906295 } } diff --git a/adapters/datablocks/datablockstest/supplemental/bad-host.json b/adapters/datablocks/datablockstest/supplemental/bad-host.json deleted file mode 100644 index cee5efbe760..00000000000 --- a/adapters/datablocks/datablockstest/supplemental/bad-host.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "mockBidRequest": { - "id": "bad-host-test", - "imp": [ - { - "id": "bad-host-test-imp", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": - { - "bidder": - { - "host": "", - "sourceId": 123 - } - } - } - ] - }, - - "expectedMakeRequestsErrors": [ - { - "value": "Invalid/Missing Host", - "comparison": "literal" - } - ] -} diff --git a/adapters/datablocks/datablockstest/supplemental/bad-response-body.json b/adapters/datablocks/datablockstest/supplemental/bad-response-body.json index 7c4801e1685..e60c6aabb7d 100644 --- a/adapters/datablocks/datablockstest/supplemental/bad-response-body.json +++ b/adapters/datablocks/datablockstest/supplemental/bad-response-body.json @@ -17,7 +17,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 123 } } @@ -37,7 +37,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=123", + "uri": "http://pbserver.dblks.net/openrtb2?sid=123", "body": { "id": "some-request-id", @@ -56,7 +56,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 123 } } diff --git a/adapters/datablocks/datablockstest/supplemental/bad-server-response.json b/adapters/datablocks/datablockstest/supplemental/bad-server-response.json index da2924f6f21..cdcc0c37d4c 100644 --- a/adapters/datablocks/datablockstest/supplemental/bad-server-response.json +++ b/adapters/datablocks/datablockstest/supplemental/bad-server-response.json @@ -17,7 +17,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 123 } } @@ -37,7 +37,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=123", + "uri": "http://pbserver.dblks.net/openrtb2?sid=123", "body": { "id": "some-request-id", @@ -56,7 +56,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 123 } } diff --git a/adapters/datablocks/datablockstest/supplemental/bad-sourceId.json b/adapters/datablocks/datablockstest/supplemental/bad-sourceId.json index 08f2fddf568..3182e71dc84 100644 --- a/adapters/datablocks/datablockstest/supplemental/bad-sourceId.json +++ b/adapters/datablocks/datablockstest/supplemental/bad-sourceId.json @@ -16,7 +16,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 0 } } diff --git a/adapters/datablocks/datablockstest/supplemental/missing-extparam.json b/adapters/datablocks/datablockstest/supplemental/missing-extparam.json deleted file mode 100644 index d272cd5347c..00000000000 --- a/adapters/datablocks/datablockstest/supplemental/missing-extparam.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "mockBidRequest": { - "id": "missing-extbid-test", - "imp": [ - { - "id": "missing-extbid-test", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "sourceId":54326 - } - } - ] - }, - - "expectedMakeRequestsErrors": [ - { - "value": "Cannot Resolve host or sourceId: unexpected end of JSON input", - "comparison": "literal" - } - ] - - -} diff --git a/adapters/datablocks/datablockstest/supplemental/no-content-response.json b/adapters/datablocks/datablockstest/supplemental/no-content-response.json index 7fc1b75c3ad..0e53f4720a9 100644 --- a/adapters/datablocks/datablockstest/supplemental/no-content-response.json +++ b/adapters/datablocks/datablockstest/supplemental/no-content-response.json @@ -17,7 +17,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 123 } } @@ -37,7 +37,7 @@ { "expectedRequest": { - "uri": "http://search.nutella.datablocks.net/openrtb2?sid=123", + "uri": "http://pbserver.dblks.net/openrtb2?sid=123", "body": { "id": "some-request-id", @@ -56,7 +56,7 @@ { "bidder": { - "host": "search.nutella.datablocks.net", + "host": "pbserver.dblks.net", "sourceId": 123 } } diff --git a/adapters/deepintent/deepintenttest/supplemental/wrongimp.json b/adapters/deepintent/deepintenttest/supplemental/wrongimp.json index 5021c51da34..a8205217b42 100644 --- a/adapters/deepintent/deepintenttest/supplemental/wrongimp.json +++ b/adapters/deepintent/deepintenttest/supplemental/wrongimp.json @@ -111,6 +111,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to find impression test-imp-id1 ", @@ -118,4 +119,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/definemedia/definemediatest/supplemental/nobid-response.json b/adapters/definemedia/definemediatest/supplemental/nobid-response.json index 2140ee9b2bf..aaa942da76e 100644 --- a/adapters/definemedia/definemediatest/supplemental/nobid-response.json +++ b/adapters/definemedia/definemediatest/supplemental/nobid-response.json @@ -218,5 +218,5 @@ } } ], - "expectedBidResponses": [] - } \ No newline at end of file + "expectedBidResponses": [{"currency":"USD","bids":[]}] + } diff --git a/adapters/definemedia/definemediatest/supplemental/unsupported-type.json b/adapters/definemedia/definemediatest/supplemental/unsupported-type.json index 0c1add16a64..91bfbc413c1 100644 --- a/adapters/definemedia/definemediatest/supplemental/unsupported-type.json +++ b/adapters/definemedia/definemediatest/supplemental/unsupported-type.json @@ -240,11 +240,11 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Invalid mediatype in the impression", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/dianomi/dianomitest/supplemental/invalid-imp-mediatype.json b/adapters/dianomi/dianomitest/supplemental/invalid-imp-mediatype.json index f12b62b03dc..1b7b522a600 100644 --- a/adapters/dianomi/dianomitest/supplemental/invalid-imp-mediatype.json +++ b/adapters/dianomi/dianomitest/supplemental/invalid-imp-mediatype.json @@ -79,7 +79,7 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to parse impression \"test-imp-id\" mediatype", diff --git a/adapters/dianomi/dianomitest/supplemental/nobid-response.json b/adapters/dianomi/dianomitest/supplemental/nobid-response.json index 7654e803b26..7a56456eea6 100644 --- a/adapters/dianomi/dianomitest/supplemental/nobid-response.json +++ b/adapters/dianomi/dianomitest/supplemental/nobid-response.json @@ -44,6 +44,6 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [] } diff --git a/adapters/emtv/emtv.go b/adapters/emtv/emtv.go new file mode 100644 index 00000000000..5dbd1e1bde7 --- /dev/null +++ b/adapters/emtv/emtv.go @@ -0,0 +1,163 @@ +package emtv + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + EmtvBidderExt reqBodyExtBidder `json:"bidder"` +} + +type reqBodyExtBidder struct { + Type string `json:"type"` + PlacementID string `json:"placementId,omitempty"` + EndpointID string `json:"endpointId,omitempty"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var err error + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + var bidderExt adapters.ExtImpBidder + var emtvExt openrtb_ext.ImpExtEmtv + + if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil { + return nil, []error{err} + } + if err = json.Unmarshal(bidderExt.Bidder, &emtvExt); err != nil { + return nil, []error{err} + } + + impExt := reqBodyExt{EmtvBidderExt: reqBodyExtBidder{}} + + if emtvExt.PlacementID != "" { + impExt.EmtvBidderExt.PlacementID = emtvExt.PlacementID + impExt.EmtvBidderExt.Type = "publisher" + } else if emtvExt.EndpointID != "" { + impExt.EmtvBidderExt.EndpointID = emtvExt.EndpointID + impExt.EmtvBidderExt.Type = "network" + } else { + continue + } + + finalyImpExt, err := json.Marshal(impExt) + if err != nil { + return nil, []error{err} + } + + reqCopy.Imp[0].Ext = finalyImpExt + + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + return nil, []error{err} + } + + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + } + + if len(adapterRequests) == 0 { + return nil, []error{errors.New("found no valid impressions")} + } + + return adapterRequests, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + + impsMappedByID := make(map[string]openrtb2.Imp, len(request.Imp)) + for i, imp := range request.Imp { + impsMappedByID[request.Imp[i].ID] = imp + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, request.Imp, impsMappedByID) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getMediaTypeForImp(impID string, imps []openrtb2.Imp, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) { + if index, found := impMap[impID]; found { + if index.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + if index.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } + if index.Native != nil { + return openrtb_ext.BidTypeNative, nil + } + } + + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\"", impID), + } +} diff --git a/adapters/emtv/emtv_test.go b/adapters/emtv/emtv_test.go new file mode 100644 index 00000000000..b1287357681 --- /dev/null +++ b/adapters/emtv/emtv_test.go @@ -0,0 +1,20 @@ +package emtv + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderEmtv, config.Adapter{ + Endpoint: "http://example.com/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "emtvtest", bidder) +} diff --git a/adapters/emtv/emtvtest/exemplary/endpointId.json b/adapters/emtv/emtvtest/exemplary/endpointId.json new file mode 100644 index 00000000000..ef73cb84cb8 --- /dev/null +++ b/adapters/emtv/emtvtest/exemplary/endpointId.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "emtv" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/emtv/emtvtest/exemplary/simple-banner.json b/adapters/emtv/emtvtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..4399da28a9e --- /dev/null +++ b/adapters/emtv/emtvtest/exemplary/simple-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "emtv" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/emtv/emtvtest/exemplary/simple-native.json b/adapters/emtv/emtvtest/exemplary/simple-native.json new file mode 100644 index 00000000000..63bb7c73cf8 --- /dev/null +++ b/adapters/emtv/emtvtest/exemplary/simple-native.json @@ -0,0 +1,117 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "emtv" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/emtv/emtvtest/exemplary/simple-video.json b/adapters/emtv/emtvtest/exemplary/simple-video.json new file mode 100644 index 00000000000..9bb3c20e254 --- /dev/null +++ b/adapters/emtv/emtvtest/exemplary/simple-video.json @@ -0,0 +1,128 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "emtv" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/emtv/emtvtest/exemplary/simple-web-banner.json b/adapters/emtv/emtvtest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..458ee258dff --- /dev/null +++ b/adapters/emtv/emtvtest/exemplary/simple-web-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "emtv" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/emtv/emtvtest/supplemental/bad_media_type.json b/adapters/emtv/emtvtest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..5a565246930 --- /dev/null +++ b/adapters/emtv/emtvtest/supplemental/bad_media_type.json @@ -0,0 +1,86 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "emtv" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Failed to find impression \"test-imp-id\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/emtv/emtvtest/supplemental/bad_response.json b/adapters/emtv/emtvtest/supplemental/bad_response.json new file mode 100644 index 00000000000..8c1cc3750db --- /dev/null +++ b/adapters/emtv/emtvtest/supplemental/bad_response.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/emtv/emtvtest/supplemental/no-valid-impressions.json b/adapters/emtv/emtvtest/supplemental/no-valid-impressions.json new file mode 100644 index 00000000000..cc1edd685f9 --- /dev/null +++ b/adapters/emtv/emtvtest/supplemental/no-valid-impressions.json @@ -0,0 +1,20 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "found no valid impressions", + "comparison": "literal" + } + ] +} diff --git a/adapters/emtv/emtvtest/supplemental/status-204.json b/adapters/emtv/emtvtest/supplemental/status-204.json new file mode 100644 index 00000000000..3e536c25a60 --- /dev/null +++ b/adapters/emtv/emtvtest/supplemental/status-204.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/emtv/emtvtest/supplemental/status-not-200.json b/adapters/emtv/emtvtest/supplemental/status-not-200.json new file mode 100644 index 00000000000..4bb3c8d5b53 --- /dev/null +++ b/adapters/emtv/emtvtest/supplemental/status-not-200.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/emtv/params_test.go b/adapters/emtv/params_test.go new file mode 100644 index 00000000000..966dd7dd460 --- /dev/null +++ b/adapters/emtv/params_test.go @@ -0,0 +1,47 @@ +package emtv + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderEmtv, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderEmtv, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "test"}`, + `{"placementId": "1"}`, + `{"endpointId": "test"}`, + `{"endpointId": "1"}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"endpointId": 42}`, + `{"placementId": "1", "endpointId": "1"}`, +} diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json index 782722be801..acbd256a9a4 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json @@ -47,5 +47,5 @@ } } ], - "expectedBidResponses": [] -} \ No newline at end of file + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/flipp/flipp.go b/adapters/flipp/flipp.go new file mode 100644 index 00000000000..14f31a91efb --- /dev/null +++ b/adapters/flipp/flipp.go @@ -0,0 +1,225 @@ +package flipp + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/buger/jsonparser" + "github.com/gofrs/uuid" + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + bannerType = "banner" + inlineDivName = "inline" + flippBidder = "flipp" + defaultCurrency = "USD" +) + +var ( + count int64 = 1 + adTypes = []int64{4309, 641} + dtxTypes = []int64{5061} +) + +type adapter struct { + endpoint string +} + +// Builder builds a new instance of the Flipp adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + adapterRequests := make([]*adapters.RequestData, 0, len(request.Imp)) + var errors []error + + for _, imp := range request.Imp { + adapterReq, err := a.processImp(request, imp) + if err != nil { + errors = append(errors, err) + continue + } + adapterRequests = append(adapterRequests, adapterReq) + } + if len(adapterRequests) == 0 { + return nil, append(errors, fmt.Errorf("adapterRequest is empty")) + } + return adapterRequests, errors +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest, campaignRequestBody CampaignRequestBody) (*adapters.RequestData, error) { + campaignRequestBodyJSON, err := json.Marshal(campaignRequestBody) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json") + if request.Device != nil && request.Device.UA != "" { + headers.Add("User-Agent", request.Device.UA) + } + return &adapters.RequestData{ + Method: http.MethodPost, + Uri: a.endpoint, + Body: campaignRequestBodyJSON, + Headers: headers, + }, err +} + +func (a *adapter) processImp(request *openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) { + var flippExtParams openrtb_ext.ImpExtFlipp + params, _, _, err := jsonparser.Get(imp.Ext, "bidder") + if err != nil { + return nil, fmt.Errorf("flipp params not found. %v", err) + } + err = json.Unmarshal(params, &flippExtParams) + if err != nil { + return nil, fmt.Errorf("unable to extract flipp params. %v", err) + } + + publisherUrl, err := url.Parse(request.Site.Page) + if err != nil { + return nil, fmt.Errorf("unable to parse site url. %v", err) + } + + var contentCode string + if flippExtParams.Options.ContentCode != "" { + contentCode = flippExtParams.Options.ContentCode + } else if publisherUrl != nil { + contentCode = publisherUrl.Query().Get("flipp-content-code") + } + + placement := Placement{ + DivName: inlineDivName, + SiteID: &flippExtParams.SiteID, + AdTypes: getAdTypes(flippExtParams.CreativeType), + ZoneIds: flippExtParams.ZoneIds, + Count: &count, + Prebid: buildPrebidRequest(flippExtParams, request, imp), + Properties: &Properties{ + ContentCode: &contentCode, + }, + Options: flippExtParams.Options, + } + + var userIP string + if request.Device != nil && request.Device.IP != "" { + userIP = request.Device.IP + } else { + return nil, fmt.Errorf("no IP set in flipp bidder params or request device") + } + + var userKey string + if request.User != nil && request.User.ID != "" { + userKey = request.User.ID + } else if flippExtParams.UserKey != "" { + userKey = flippExtParams.UserKey + } else { + uid, err := uuid.NewV4() + if err != nil { + return nil, fmt.Errorf("unable to generate user uuid. %v", err) + } + userKey = uid.String() + } + + keywordsArray := strings.Split(request.Site.Keywords, ",") + + campaignRequestBody := CampaignRequestBody{ + Placements: []*Placement{&placement}, + URL: request.Site.Page, + Keywords: keywordsArray, + IP: userIP, + User: &CampaignRequestBodyUser{ + Key: &userKey, + }, + } + + adapterReq, err := a.makeRequest(request, campaignRequestBody) + if err != nil { + return nil, fmt.Errorf("make request failed with err %v", err) + } + + return adapterReq, nil +} + +func buildPrebidRequest(flippExtParams openrtb_ext.ImpExtFlipp, request *openrtb2.BidRequest, imp openrtb2.Imp) *PrebidRequest { + var height int64 + var width int64 + if imp.Banner != nil && len(imp.Banner.Format) > 0 { + height = imp.Banner.Format[0].H + width = imp.Banner.Format[0].W + } + prebidRequest := PrebidRequest{ + CreativeType: &flippExtParams.CreativeType, + PublisherNameIdentifier: &flippExtParams.PublisherNameIdentifier, + RequestID: &imp.ID, + Height: &height, + Width: &width, + } + return &prebidRequest +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var campaignResponseBody CampaignResponseBody + if err := json.Unmarshal(responseData.Body, &campaignResponseBody); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = defaultCurrency + for _, imp := range request.Imp { + for _, decision := range campaignResponseBody.Decisions.Inline { + if *decision.Prebid.RequestID == imp.ID { + b := &adapters.TypedBid{ + Bid: buildBid(decision, imp.ID), + BidType: openrtb_ext.BidType(bannerType), + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, nil +} + +func getAdTypes(creativeType string) []int64 { + if creativeType == "DTX" { + return dtxTypes + } + return adTypes +} + +func buildBid(decision *InlineModel, impId string) *openrtb2.Bid { + bid := &openrtb2.Bid{ + CrID: fmt.Sprint(decision.CreativeID), + Price: *decision.Prebid.Cpm, + AdM: *decision.Prebid.Creative, + ID: fmt.Sprint(decision.AdID), + ImpID: impId, + } + if len(decision.Contents) > 0 || decision.Contents[0] != nil || decision.Contents[0].Data != nil { + if decision.Contents[0].Data.Width != 0 { + bid.W = decision.Contents[0].Data.Width + } + bid.H = 0 + } + return bid +} diff --git a/adapters/flipp/flipp_params.go b/adapters/flipp/flipp_params.go new file mode 100644 index 00000000000..0eef61b4b89 --- /dev/null +++ b/adapters/flipp/flipp_params.go @@ -0,0 +1,88 @@ +package flipp + +import "github.com/prebid/prebid-server/openrtb_ext" + +type CampaignRequestBodyUser struct { + Key *string `json:"key"` +} + +type Properties struct { + ContentCode *string `json:"contentCode,omitempty"` +} + +type PrebidRequest struct { + CreativeType *string `json:"creativeType"` + Height *int64 `json:"height"` + PublisherNameIdentifier *string `json:"publisherNameIdentifier"` + RequestID *string `json:"requestId"` + Width *int64 `json:"width"` +} + +type Placement struct { + AdTypes []int64 `json:"adTypes"` + Count *int64 `json:"count"` + DivName string `json:"divName,omitempty"` + NetworkID int64 `json:"networkId,omitempty"` + Prebid *PrebidRequest `json:"prebid,omitempty"` + Properties *Properties `json:"properties,omitempty"` + SiteID *int64 `json:"siteId"` + ZoneIds []int64 `json:"zoneIds"` + Options openrtb_ext.ImpExtFlippOptions `json:"options,omitempty"` +} + +type CampaignRequestBody struct { + IP string `json:"ip,omitempty"` + Keywords []string `json:"keywords"` + Placements []*Placement `json:"placements"` + PreferredLanguage *string `json:"preferred_language,omitempty"` + URL string `json:"url,omitempty"` + User *CampaignRequestBodyUser `json:"user"` +} + +type CampaignResponseBody struct { + CandidateRetrieval interface{} `json:"candidateRetrieval,omitempty"` + Decisions *Decisions `json:"decisions"` +} + +type Decisions struct { + Inline Inline `json:"inline,omitempty"` +} + +type Inline []*InlineModel + +type Contents []*Content + +type Content struct { + Body string `json:"body,omitempty"` + CustomTemplate string `json:"customTemplate,omitempty"` + Data *Data2 `json:"data,omitempty"` + Type string `json:"type,omitempty"` +} + +type Data2 struct { + CustomData interface{} `json:"customData,omitempty"` + Height int64 `json:"height,omitempty"` + Width int64 `json:"width,omitempty"` +} + +type InlineModel struct { + AdID int64 `json:"adId,omitempty"` + AdvertiserID int64 `json:"advertiserId,omitempty"` + CampaignID int64 `json:"campaignId,omitempty"` + ClickURL string `json:"clickUrl,omitempty"` + Contents Contents `json:"contents,omitempty"` + CreativeID int64 `json:"creativeId,omitempty"` + FlightID int64 `json:"flightId,omitempty"` + Height int64 `json:"height,omitempty"` + ImpressionURL string `json:"impressionUrl,omitempty"` + Prebid *PrebidResponse `json:"prebid,omitempty"` + PriorityID int64 `json:"priorityId,omitempty"` + Width int64 `json:"width,omitempty"` +} + +type PrebidResponse struct { + Cpm *float64 `json:"cpm"` + Creative *string `json:"creative"` + CreativeType *string `json:"creativeType"` + RequestID *string `json:"requestId"` +} diff --git a/adapters/flipp/flipp_test.go b/adapters/flipp/flipp_test.go new file mode 100644 index 00000000000..79b79e20769 --- /dev/null +++ b/adapters/flipp/flipp_test.go @@ -0,0 +1,21 @@ +package flipp + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderFlipp, config.Adapter{ + Endpoint: "http://example.com/pserver"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "flipptest", bidder) +} diff --git a/adapters/flipp/flipptest/exemplary/simple-banner-dtx.json b/adapters/flipp/flipptest/exemplary/simple-banner-dtx.json new file mode 100644 index 00000000000..6bdad61d6b2 --- /dev/null +++ b/adapters/flipp/flipptest/exemplary/simple-banner-dtx.json @@ -0,0 +1,173 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + "device": { + "ip": "123.123.123.123" + }, + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + "id": "1234" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + "bidder": { + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "DTX", + "siteId": 1243066, + "zoneIds": [285431], + "options": { + "startCompact": true + } + } + } + } + + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "ip":"123.123.123.123", + "keywords":[ + "" + ], + "placements":[ + { + "adTypes":[ + 5061 + ], + "count":1, + "divName":"inline", + "prebid":{ + "creativeType":"DTX", + "height":1800, + "publisherNameIdentifier":"wishabi-test-publisher", + "requestId":"test-imp-id", + "width":300 + }, + "properties":{ + "contentCode":"publisher-test" + }, + "siteId":1243066, + "zoneIds":[ + 285431 + ], + "options": { + "startCompact": true + } + } + ], + "url":"http://www.example.com/test?flipp-content-code=publisher-test", + "user":{ + "key":"1234" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "decisions": { + "inline": [ + { + "adId": 183599115, + "advertiserId": 1988027, + "campaignId": 63285392, + "clickUrl": "https://e-11090.adzerk.net/r?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMiwicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsInVyIjoiaHR0cDovL3d3dy5mbGlwcC5jb20ifQ&s=Mnss8P1kc37Eftik5RJvLJb4S9Y", + "contents": [ + { + "data": { + "customData": { + "campaignConfigUrl": "https://campaign-config.flipp.com/dist-campaign-admin/215", + "campaignNameIdentifier": "US Grocery Demo (Kroger)" + }, + "height": 1800, + "width": 300 + }, + "type": "raw" + } + ], + "creativeId": 81325690, + "flightId": 175421795, + "height": 1800, + "impressionUrl": "https://e-11090.adzerk.net/i.gif?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMywicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsImJhIjoxLCJmcSI6MH0&s=Qce4_IohtESeNA_sB71Qjb4TouY", + "prebid": { + "cpm": 12.34, + "creative":"creativeContent", + "creativeType": "DTX", + "requestId": "test-imp-id" + }, + "priorityId": 232699, + "storefront": { + "campaignConfig": { + "fallbackImageUrl": "https://f.wishabi.net/arbitrary_files/115856/1666018811/115856_Featured Local Savings - Flipp Fallback - US (1).png", + "fallbackLinkUrl": "https://flipp.com/flyers/", + "tags": { + "advertiser": { + "engagement": "https://f.wishabi.net/creative/happyfruits/_itemlevelv2/apple.png?cb=[[random]]" + } + } + }, + "flyer_id": 5554080, + "flyer_run_id": 873256, + "flyer_type_id": 2826, + "is_fallback": true, + "merchant": "Kroger", + "merchant_id": 2778, + "name": "Weekly Ad", + "storefront_logo_url": "https://images.wishabi.net/merchants/qX1/BGIzc9sFcA==/RackMultipart20210421-1-e3k5rx.jpeg", + "storefront_payload_url": "https://cdn-gateflipp.flippback.com/storefront-payload/v2/873256/5554080/ff14f675705934507c269b5750e124a323bc9bf60e8a6f210f422f4528b233ff?merchant_id=2778", + "valid_from": "2023-03-29 04:00:00 +0000 UTC", + "valid_to": "2023-04-05 03:59:59 +0000 UTC" + }, + "width": 300 + } + ] + }, + "location": { + "accuracy_radius": 5, + "city": "Toronto", + "country": "CA", + "ip": "142.181.58.52", + "postal_code": "M4S", + "region": "ON", + "region_name": "Ontario" + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "183599115", + "impid": "test-imp-id", + "price": 12.34, + "adm": "creativeContent", + "crid": "81325690", + "w": 300 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/flipp/flipptest/exemplary/simple-banner-native-param-user.json b/adapters/flipp/flipptest/exemplary/simple-banner-native-param-user.json new file mode 100644 index 00000000000..272bcd744ab --- /dev/null +++ b/adapters/flipp/flipptest/exemplary/simple-banner-native-param-user.json @@ -0,0 +1,174 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + "device": { + "ip": "123.123.123.123" + }, + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + "bidder": { + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431], + "options": { + "startCompact": true + }, + "userKey": "abc123" + } + } + } + + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "ip":"123.123.123.123", + "keywords":[ + "" + ], + "placements":[ + { + "adTypes":[ + 4309, + 641 + ], + "count":1, + "divName":"inline", + "prebid":{ + "creativeType":"NativeX", + "height":1800, + "publisherNameIdentifier":"wishabi-test-publisher", + "requestId":"test-imp-id", + "width":300 + }, + "properties":{ + "contentCode":"publisher-test" + }, + "siteId":1243066, + "zoneIds":[ + 285431 + ], + "options": { + "startCompact": true + } + } + ], + "url":"http://www.example.com/test?flipp-content-code=publisher-test", + "user":{ + "key":"abc123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "decisions": { + "inline": [ + { + "adId": 183599115, + "advertiserId": 1988027, + "campaignId": 63285392, + "clickUrl": "https://e-11090.adzerk.net/r?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMiwicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsInVyIjoiaHR0cDovL3d3dy5mbGlwcC5jb20ifQ&s=Mnss8P1kc37Eftik5RJvLJb4S9Y", + "contents": [ + { + "data": { + "customData": { + "campaignConfigUrl": "https://campaign-config.flipp.com/dist-campaign-admin/215", + "campaignNameIdentifier": "US Grocery Demo (Kroger)" + }, + "height": 1800, + "width": 300 + }, + "type": "raw" + } + ], + "creativeId": 81325690, + "flightId": 175421795, + "height": 1800, + "impressionUrl": "https://e-11090.adzerk.net/i.gif?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMywicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsImJhIjoxLCJmcSI6MH0&s=Qce4_IohtESeNA_sB71Qjb4TouY", + "prebid": { + "cpm": 12.34, + "creative":"creativeContent", + "creativeType": "NativeX", + "requestId": "test-imp-id" + }, + "priorityId": 232699, + "storefront": { + "campaignConfig": { + "fallbackImageUrl": "https://f.wishabi.net/arbitrary_files/115856/1666018811/115856_Featured Local Savings - Flipp Fallback - US (1).png", + "fallbackLinkUrl": "https://flipp.com/flyers/", + "tags": { + "advertiser": { + "engagement": "https://f.wishabi.net/creative/happyfruits/_itemlevelv2/apple.png?cb=[[random]]" + } + } + }, + "flyer_id": 5554080, + "flyer_run_id": 873256, + "flyer_type_id": 2826, + "is_fallback": true, + "merchant": "Kroger", + "merchant_id": 2778, + "name": "Weekly Ad", + "storefront_logo_url": "https://images.wishabi.net/merchants/qX1/BGIzc9sFcA==/RackMultipart20210421-1-e3k5rx.jpeg", + "storefront_payload_url": "https://cdn-gateflipp.flippback.com/storefront-payload/v2/873256/5554080/ff14f675705934507c269b5750e124a323bc9bf60e8a6f210f422f4528b233ff?merchant_id=2778", + "valid_from": "2023-03-29 04:00:00 +0000 UTC", + "valid_to": "2023-04-05 03:59:59 +0000 UTC" + }, + "width": 300 + } + ] + }, + "location": { + "accuracy_radius": 5, + "city": "Toronto", + "country": "CA", + "ip": "123.123.123.123", + "postal_code": "M4S", + "region": "ON", + "region_name": "Ontario" + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "183599115", + "impid": "test-imp-id", + "price": 12.34, + "adm": "creativeContent", + "crid": "81325690", + "w": 300 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/flipp/flipptest/exemplary/simple-banner-native.json b/adapters/flipp/flipptest/exemplary/simple-banner-native.json new file mode 100644 index 00000000000..d8a24131c77 --- /dev/null +++ b/adapters/flipp/flipptest/exemplary/simple-banner-native.json @@ -0,0 +1,176 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + "device": { + "ip": "123.123.123.123" + }, + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + "id": "1234" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + "bidder": { + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431], + "options": { + "startCompact": true, + "contentCode": "publisher-test-2" + } + } + } + } + + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "ip":"123.123.123.123", + "keywords":[ + "" + ], + "placements":[ + { + "adTypes":[ + 4309, + 641 + ], + "count":1, + "divName":"inline", + "prebid":{ + "creativeType":"NativeX", + "height":1800, + "publisherNameIdentifier":"wishabi-test-publisher", + "requestId":"test-imp-id", + "width":300 + }, + "properties":{ + "contentCode":"publisher-test-2" + }, + "siteId":1243066, + "zoneIds":[ + 285431 + ], + "options": { + "startCompact": true, + "contentCode": "publisher-test-2" + } + } + ], + "url":"http://www.example.com/test?flipp-content-code=publisher-test", + "user":{ + "key":"1234" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "decisions": { + "inline": [ + { + "adId": 183599115, + "advertiserId": 1988027, + "campaignId": 63285392, + "clickUrl": "https://e-11090.adzerk.net/r?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMiwicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsInVyIjoiaHR0cDovL3d3dy5mbGlwcC5jb20ifQ&s=Mnss8P1kc37Eftik5RJvLJb4S9Y", + "contents": [ + { + "data": { + "customData": { + "campaignConfigUrl": "https://campaign-config.flipp.com/dist-campaign-admin/215", + "campaignNameIdentifier": "US Grocery Demo (Kroger)" + }, + "height": 1800, + "width": 300 + }, + "type": "raw" + } + ], + "creativeId": 81325690, + "flightId": 175421795, + "height": 1800, + "impressionUrl": "https://e-11090.adzerk.net/i.gif?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMywicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsImJhIjoxLCJmcSI6MH0&s=Qce4_IohtESeNA_sB71Qjb4TouY", + "prebid": { + "cpm": 12.34, + "creative":"creativeContent", + "creativeType": "NativeX", + "requestId": "test-imp-id" + }, + "priorityId": 232699, + "storefront": { + "campaignConfig": { + "fallbackImageUrl": "https://f.wishabi.net/arbitrary_files/115856/1666018811/115856_Featured Local Savings - Flipp Fallback - US (1).png", + "fallbackLinkUrl": "https://flipp.com/flyers/", + "tags": { + "advertiser": { + "engagement": "https://f.wishabi.net/creative/happyfruits/_itemlevelv2/apple.png?cb=[[random]]" + } + } + }, + "flyer_id": 5554080, + "flyer_run_id": 873256, + "flyer_type_id": 2826, + "is_fallback": true, + "merchant": "Kroger", + "merchant_id": 2778, + "name": "Weekly Ad", + "storefront_logo_url": "https://images.wishabi.net/merchants/qX1/BGIzc9sFcA==/RackMultipart20210421-1-e3k5rx.jpeg", + "storefront_payload_url": "https://cdn-gateflipp.flippback.com/storefront-payload/v2/873256/5554080/ff14f675705934507c269b5750e124a323bc9bf60e8a6f210f422f4528b233ff?merchant_id=2778", + "valid_from": "2023-03-29 04:00:00 +0000 UTC", + "valid_to": "2023-04-05 03:59:59 +0000 UTC" + }, + "width": 300 + } + ] + }, + "location": { + "accuracy_radius": 5, + "city": "Toronto", + "country": "CA", + "ip": "123.123.123.124", + "postal_code": "M4S", + "region": "ON", + "region_name": "Ontario" + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "183599115", + "impid": "test-imp-id", + "price": 12.34, + "adm": "creativeContent", + "crid": "81325690", + "w": 300 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/flipp/flipptest/supplemental/bad-response.json b/adapters/flipp/flipptest/supplemental/bad-response.json new file mode 100644 index 00000000000..764932ecb6e --- /dev/null +++ b/adapters/flipp/flipptest/supplemental/bad-response.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + "device": { + "ip": "123.123.123.123" + }, + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + "id": "1234" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + "bidder": { + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431] + } + } + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "ip":"123.123.123.123", + "keywords":[ + "" + ], + "placements":[ + { + "adTypes":[ + 4309, + 641 + ], + "count":1, + "divName":"inline", + "prebid":{ + "creativeType":"NativeX", + "height":1800, + "publisherNameIdentifier":"wishabi-test-publisher", + "requestId":"test-imp-id", + "width":300 + }, + "properties":{ + "contentCode":"publisher-test" + }, + "options":{}, + "siteId":1243066, + "zoneIds":[ + 285431 + ] + } + ], + "url":"http://www.example.com/test?flipp-content-code=publisher-test", + "user":{ + "key":"1234" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type flipp.CampaignResponseBody", + "comparison": "literal" + } + ] +} diff --git a/adapters/flipp/flipptest/supplemental/flipp-params-not-found.json b/adapters/flipp/flipptest/supplemental/flipp-params-not-found.json new file mode 100644 index 00000000000..a559e8d5126 --- /dev/null +++ b/adapters/flipp/flipptest/supplemental/flipp-params-not-found.json @@ -0,0 +1,44 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + "device": { + "ip": "123.123.123.123" + }, + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + "id": "1234" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + + } + } + + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "flipp params not found. Key path not found", + "comparison": "literal" + }, + { + "value": "adapterRequest is empty", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/flipp/flipptest/supplemental/no-ip-error.json b/adapters/flipp/flipptest/supplemental/no-ip-error.json new file mode 100644 index 00000000000..37120161e71 --- /dev/null +++ b/adapters/flipp/flipptest/supplemental/no-ip-error.json @@ -0,0 +1,50 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + "id": "1234" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + "bidder": { + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431], + "options": { + "startCompact": true + } + } + } + } + + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "no IP set in flipp bidder params or request device", + "comparison": "literal" + }, + { + "value": "adapterRequest is empty", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/flipp/flipptest/supplemental/status-not-200.json b/adapters/flipp/flipptest/supplemental/status-not-200.json new file mode 100644 index 00000000000..5039a905e47 --- /dev/null +++ b/adapters/flipp/flipptest/supplemental/status-not-200.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "test": 1, + "device": { + "ip": "123.123.123.123" + }, + "site": { + "id": "1243066", + "page": "http://www.example.com/test?flipp-content-code=publisher-test" + }, + "user": { + "id": "1234" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 1800 + } + ] + }, + "ext": { + "bidder": { + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431] + } + } + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "ip":"123.123.123.123", + "keywords":[ + "" + ], + "placements":[ + { + "adTypes":[ + 4309, + 641 + ], + "count":1, + "divName":"inline", + "prebid":{ + "creativeType":"NativeX", + "height":1800, + "publisherNameIdentifier":"wishabi-test-publisher", + "requestId":"test-imp-id", + "width":300 + }, + "properties":{ + "contentCode":"publisher-test" + }, + "siteId":1243066, + "zoneIds":[ + 285431 + ], + "options": {} + } + ], + "url":"http://www.example.com/test?flipp-content-code=publisher-test", + "user":{ + "key":"1234" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/flipp/params_test.go b/adapters/flipp/params_test.go new file mode 100644 index 00000000000..56f2fadbbdd --- /dev/null +++ b/adapters/flipp/params_test.go @@ -0,0 +1,127 @@ +package flipp + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderFlipp, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderFlipp, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431] + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "options": {} + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431], + "options": { + "startCompact": true + } + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431], + "options": { + "startCompact": false, + "dwellExpand": true, + "contentCode": "test-code" + } + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431], + "ip": "123.123.123.123", + "options": { + "startCompact": false + } + }`, +} + +var invalidParams = []string{ + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "zoneIds": [285431] + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "siteId": 1243066, + "zoneIds": [285431] + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "abc", + "siteId": 1243066, + "zoneIds": [285431] + }`, + `{ + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": [285431] + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": "123abc", + "zoneIds": [285431] + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "creativeType": "NativeX", + "siteId": 1243066, + "zoneIds": ["abc123"] + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "siteId": 1243066, + "startCompact": "true" + }`, + `{ + "publisherNameIdentifier": "wishabi-test-publisher", + "siteId": 1243066, + "options": { + "startCompact": "true" + } + }`, +} diff --git a/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json b/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json index 5ec36993375..e6838240ca4 100644 --- a/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json +++ b/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json @@ -14,7 +14,7 @@ }, "ext": { "bidder": { - "zoneId": 12345 + "zoneId": "12345" } } }, diff --git a/adapters/freewheelssp/freewheelssptest/exemplary/string-single-imp.json b/adapters/freewheelssp/freewheelssptest/exemplary/string-single-imp.json new file mode 100644 index 00000000000..c17c7167d6e --- /dev/null +++ b/adapters/freewheelssp/freewheelssptest/exemplary/string-single-imp.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "id": "freewheelssp-test", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp-1", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "zoneId": "12345" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://testjsonsample.com", + "body":{ + "id": "freewheelssp-test", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp-1", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 250 + }, + "ext": { + "zoneId": 12345 + } + }] + }, + "headers": { + "Componentid": [ + "prebid-go" + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "freewheelssp-test", + "seatbid": [ + { + "bid": [ + { + "id": "12345_freewheelssp-test_1", + "impid": "imp-1", + "price": 1.0, + "adid": "7857", + "adm": "", + "cid": "4001", + "crid": "7857" + } + ], + "seat": "freewheelsspTv" + } + ], + "bidid": "freewheelssp-test", + "cur": "EUR" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "CUR", + "bids": [ + { + "bid": { + "id": "12345_freewheelssp-test_1", + "impid": "imp-1", + "price": 1.0, + "adid": "7857", + "adm": "", + "cid": "4001", + "crid": "7857" + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/freewheelssp/freewheelssptest/supplemental/zoneid_error.json b/adapters/freewheelssp/freewheelssptest/supplemental/zoneid_error.json new file mode 100644 index 00000000000..b45296ab32c --- /dev/null +++ b/adapters/freewheelssp/freewheelssptest/supplemental/zoneid_error.json @@ -0,0 +1,29 @@ +{ + "mockBidRequest": { + "id": "freewheelssp-test", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp-1", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "zoneId": "abc123" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": ".*Invalid imp.ext for impression index 0.*", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/frvradn/frvradn.go b/adapters/frvradn/frvradn.go new file mode 100644 index 00000000000..4088bf9cdf6 --- /dev/null +++ b/adapters/frvradn/frvradn.go @@ -0,0 +1,151 @@ +package frvradn + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + uri string +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + if config.Endpoint == "" { + return nil, errors.New("missing endpoint adapter parameter") + } + + bidder := &adapter{ + uri: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errs []error + + requestCopy := *request + for _, imp := range request.Imp { + frvrAdnExt, err := getImpressionExt(&imp) + if err != nil { + errs = append(errs, err) + continue + } + + if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" { + convertedValue, err := requestInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD") + if err != nil { + errs = append(errs, err) + continue + } + imp.BidFloorCur = "USD" + imp.BidFloor = convertedValue + } + + ext, err := json.Marshal(frvrAdnExt) + if err != nil { + errs = append(errs, err) + continue + } + imp.Ext = ext + + requestCopy.Imp = []openrtb2.Imp{imp} + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.uri, + Body: requestJSON, + } + requests = append(requests, requestData) + } + return requests, errs +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + if len(responseData.Body) == 0 { + return nil, nil + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errs []error + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getBidMediaType(&seatBid.Bid[i]) + if err != nil { + errs = append(errs, err) + continue + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, errs +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ImpExtFRVRAdn, error) { + var extImpBidder adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &extImpBidder); err != nil { + return nil, &errortypes.BadInput{ + Message: "missing ext", + } + } + var frvrAdnExt openrtb_ext.ImpExtFRVRAdn + if err := json.Unmarshal(extImpBidder.Bidder, &frvrAdnExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "missing ext.bidder", + } + } + + if len(frvrAdnExt.PublisherID) == 0 || len(frvrAdnExt.AdUnitID) == 0 { + return nil, &errortypes.BadInput{ + Message: "publisher_id and ad_unit_id are required", + } + } + return &frvrAdnExt, nil +} + +func getBidMediaType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { + var extBid openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &extBid) + if err != nil { + return "", fmt.Errorf("unable to deserialize imp %v bid.ext", bid.ImpID) + } + + if extBid.Prebid == nil { + return "", fmt.Errorf("imp %v with unknown media type", bid.ImpID) + } + + return extBid.Prebid.Type, nil +} diff --git a/adapters/frvradn/frvradn_test.go b/adapters/frvradn/frvradn_test.go new file mode 100644 index 00000000000..f50f319b04f --- /dev/null +++ b/adapters/frvradn/frvradn_test.go @@ -0,0 +1,28 @@ +package frvradn + +import ( + "github.com/stretchr/testify/assert" + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderFRVRAdNetwork, config.Adapter{ + Endpoint: "https://fran.frvr.com/api/v1/openrtb", + }, config.Server{ExternalUrl: "https://host.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "frvradntest", bidder) +} + +func TestInvalidEndpoint(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderFRVRAdNetwork, config.Adapter{Endpoint: ""}, config.Server{}) + + assert.Error(t, buildErr) +} diff --git a/adapters/frvradn/frvradntest/exemplary/banner.json b/adapters/frvradn/frvradntest/exemplary/banner.json new file mode 100644 index 00000000000..1c9866aa312 --- /dev/null +++ b/adapters/frvradn/frvradntest/exemplary/banner.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "4fd8fadb-b218-41cd-a99f-8ae932b8c529", + "publisher_id": "7de498f7-f955-4696-a337-348b026c5e42" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "4fd8fadb-b218-41cd-a99f-8ae932b8c529", + "publisher_id": "7de498f7-f955-4696-a337-348b026c5e42" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "d4916d88-c0f4-4340-87c0-bc9054696d3a", + "impid": "test-banner-id", + "price": 0.55, + "adm": "a creative", + "adid": "123123", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "d4916d88-c0f4-4340-87c0-bc9054696d3a", + "impid": "test-banner-id", + "price": 0.55, + "adm": "a creative", + "adid": "123123", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/frvradn/frvradntest/exemplary/native.json b/adapters/frvradn/frvradntest/exemplary/native.json new file mode 100644 index 00000000000..9b350547e94 --- /dev/null +++ b/adapters/frvradn/frvradntest/exemplary/native.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-native-id", + "native": { + "request": "test-native-request" + }, + "ext": { + "bidder": { + "ad_unit_id": "38cab549-569e-4362-a90f-f7c1d09d3e4c", + "publisher_id": "90f55919-525e-4e2f-99cb-fd4a59fb75a0" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-native-id", + "native": { + "request": "test-native-request" + }, + "ext": { + "ad_unit_id": "38cab549-569e-4362-a90f-f7c1d09d3e4c", + "publisher_id": "90f55919-525e-4e2f-99cb-fd4a59fb75a0" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "9244e776-e0c7-4d76-8c20-88971909114b", + "impid": "test-native-id", + "price": 2.50, + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "native" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "9244e776-e0c7-4d76-8c20-88971909114b", + "impid": "test-native-id", + "price": 2.50, + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/frvradn/frvradntest/exemplary/video.json b/adapters/frvradn/frvradntest/exemplary/video.json new file mode 100644 index 00000000000..9ff1bb31f8a --- /dev/null +++ b/adapters/frvradn/frvradntest/exemplary/video.json @@ -0,0 +1,101 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-video-id", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 600 + }, + "ext": { + "bidder": { + "ad_unit_id": "5da02f4f-96de-4a89-824a-6e8e1dfc495c", + "publisher_id": "9d66baf8-a80f-4378-9bd2-06ec09eaa86c" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-video-id", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 600 + }, + "ext": { + "ad_unit_id": "5da02f4f-96de-4a89-824a-6e8e1dfc495c", + "publisher_id": "9d66baf8-a80f-4378-9bd2-06ec09eaa86c" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "42d72858-e071-40b2-ab74-27848e4d3336", + "impid": "test-video-id", + "price": 6.50, + "nurl": "notification", + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "42d72858-e071-40b2-ab74-27848e4d3336", + "impid": "test-video-id", + "price": 6.50, + "nurl": "notification", + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/frvradn/frvradntest/supplemental/204.json b/adapters/frvradn/frvradntest/supplemental/204.json new file mode 100644 index 00000000000..48fe8a03aa7 --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/204.json @@ -0,0 +1,55 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "aae06006-2c41-499c-a432-7b74bc8d9b28", + "publisher_id": "0af78b6e-45ad-4bad-8332-4b5e6a87b222" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "aae06006-2c41-499c-a432-7b74bc8d9b28", + "publisher_id": "0af78b6e-45ad-4bad-8332-4b5e6a87b222" + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/frvradn/frvradntest/supplemental/400.json b/adapters/frvradn/frvradntest/supplemental/400.json new file mode 100644 index 00000000000..a7e5f40d176 --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/400.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd", + "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd", + "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6" + } + } + ] + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/frvradn/frvradntest/supplemental/503.json b/adapters/frvradn/frvradntest/supplemental/503.json new file mode 100644 index 00000000000..11b9ce2d52e --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/503.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd", + "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd", + "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6" + } + } + ] + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 503. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/frvradn/frvradntest/supplemental/currency_converter.json b/adapters/frvradn/frvradntest/supplemental/currency_converter.json new file mode 100644 index 00000000000..45561d0be16 --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/currency_converter.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 5, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 1.5 + } + }, + "usepbsrates": false + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 7.5, + "bidfloorcur": "USD", + "ext": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 1.5 + } + }, + "usepbsrates": false + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "ed03a73b-3c9d-4883-a672-7919cb607c15", + "impid": "test-banner-id", + "price": 7.5, + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "ed03a73b-3c9d-4883-a672-7919cb607c15", + "impid": "test-banner-id", + "price": 7.5, + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/frvradn/frvradntest/supplemental/empty_object_response.json b/adapters/frvradn/frvradntest/supplemental/empty_object_response.json new file mode 100644 index 00000000000..00655c70fe2 --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/empty_object_response.json @@ -0,0 +1,56 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": {} + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/frvradn/frvradntest/supplemental/empty_reponse.json b/adapters/frvradn/frvradntest/supplemental/empty_reponse.json new file mode 100644 index 00000000000..d6e2b059a6e --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/empty_reponse.json @@ -0,0 +1,55 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + ] + } + }, + "mockResponse": { + "status": 200 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json b/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json new file mode 100644 index 00000000000..e601c882dc1 --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json @@ -0,0 +1,82 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "ad_unit_id": "56f94390-e632-46e3-8e0f-e399d7d6e10d", + "publisher_id": "96ce87c5-1c9d-4456-a093-9d8162e54b44" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "ad_unit_id": "56f94390-e632-46e3-8e0f-e399d7d6e10d", + "publisher_id": "96ce87c5-1c9d-4456-a093-9d8162e54b44" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "ec174d08-4c15-4ea0-95cc-e55bb5bfa0f9\n\n", + "impid": "random-imp", + "price": 0.50, + "adm": "a creative", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "unable to deserialize imp random-imp bid.ext", + "comparison": "literal" + } + ] +} diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json b/adapters/frvradn/frvradntest/supplemental/missing_ext.json similarity index 55% rename from adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json rename to adapters/frvradn/frvradntest/supplemental/missing_ext.json index da48108af0b..4c22142b1e0 100644 --- a/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json +++ b/adapters/frvradn/frvradntest/supplemental/missing_ext.json @@ -3,32 +3,22 @@ "id": "test-request-id", "imp": [ { - "id": "test-missing-req-param-id", + "id": "test-banner-id", "banner": { "format": [ - { - "w": 300, - "h": 250 - }, { "w": 300, "h": 600 } ] - }, - "ext": { - "bidder": { - "publisher":"test" - } } - } ] }, "expectedMakeRequestsErrors": [ { - "value": "Invalid publisher", + "value": "missing ext", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/brightroll/brightrolltest/supplemental/missing-param.json b/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json similarity index 56% rename from adapters/brightroll/brightrolltest/supplemental/missing-param.json rename to adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json index 08e82c0e31c..b88f989a3e3 100644 --- a/adapters/brightroll/brightrolltest/supplemental/missing-param.json +++ b/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json @@ -3,31 +3,22 @@ "id": "test-request-id", "imp": [ { - "id": "test-missing-req-param-id", + "id": "test-banner-id", "banner": { "format": [ - { - "w": 300, - "h": 250 - }, { "w": 300, "h": 600 } ] }, - "ext": { - "bidder": { - "publisher":"" - } - } - + "ext": {} } ] }, "expectedMakeRequestsErrors": [ { - "value": "publisher is empty", + "value": "missing ext.bidder", "comparison": "literal" } ] diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-extension.json b/adapters/frvradn/frvradntest/supplemental/missing_params.json similarity index 67% rename from adapters/brightroll/brightrolltest/supplemental/invalid-extension.json rename to adapters/frvradn/frvradntest/supplemental/missing_params.json index 35b1563822f..4aba47d2464 100644 --- a/adapters/brightroll/brightrolltest/supplemental/invalid-extension.json +++ b/adapters/frvradn/frvradntest/supplemental/missing_params.json @@ -3,13 +3,9 @@ "id": "test-request-id", "imp": [ { - "id": "test-invalid-ext-id", + "id": "test-banner-id", "banner": { "format": [ - { - "w": 300, - "h": 250 - }, { "w": 300, "h": 600 @@ -17,14 +13,14 @@ ] }, "ext": { + "bidder": {} } - } ] }, "expectedMakeRequestsErrors": [ { - "value": "ext.bidder.publisher not provided", + "value": "publisher_id and ad_unit_id are required", "comparison": "literal" } ] diff --git a/adapters/frvradn/frvradntest/supplemental/unknown_currency.json b/adapters/frvradn/frvradntest/supplemental/unknown_currency.json new file mode 100644 index 00000000000..74332ad6faa --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/unknown_currency.json @@ -0,0 +1,44 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 1, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693", + "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "GBP": { + "USD": 1.5 + } + }, + "usepbsrates": false + } + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Currency conversion rate not found: 'EUR' => 'USD'", + "comparison": "literal" + } + ] +} diff --git a/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json b/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json new file mode 100644 index 00000000000..5b8dfb62d1c --- /dev/null +++ b/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json @@ -0,0 +1,67 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "ext": { + "bidder": { + "ad_unit_id": "cdb3a029-ce0a-4b4b-b404-1d5f9ff86d7d", + "publisher_id": "dc4c0f5e-8637-4017-8aa8-af59d4bc03af" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fran.frvr.com/api/v1/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-id", + "ext": { + "ad_unit_id": "cdb3a029-ce0a-4b4b-b404-1d5f9ff86d7d", + "publisher_id": "dc4c0f5e-8637-4017-8aa8-af59d4bc03af" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "e2710817-83dd-421f-9cd6-55bed1ca8a41", + "impid": "test-banner-id", + "price": 5.50, + "adm": "test-banner", + "adid": "9999", + "adomain": ["advertiser.com"], + "crid": "9999", + "w": 300, + "h": 600, + "ext": {} + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "imp test-banner-id with unknown media type", + "comparison": "literal" + } + ] +} diff --git a/adapters/frvradn/params_test.go b/adapters/frvradn/params_test.go new file mode 100644 index 00000000000..2d4836c1e13 --- /dev/null +++ b/adapters/frvradn/params_test.go @@ -0,0 +1,48 @@ +package frvradn + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderFRVRAdNetwork, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderFRVRAdNetwork, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"publisher_id": "247f36ed-bda5-4159-86f1-e383849e7810", "ad_unit_id": "63c36c82-3246-4931-97f9-4f16a9639ba9"}`, +} + +var invalidParams = []string{ + `null`, + `nil`, + ``, + `[]`, + `{}`, + `{"publisher_id": "247f36ed-bda5-4159-86f1-e383849e7810"}`, + `{"ad_unit_id": "247f36ed-bda5-4159-86f1-e383849e7810"}`, +} diff --git a/adapters/gamma/gammatest/supplemental/ignore-imp.json b/adapters/gamma/gammatest/supplemental/ignore-imp.json index 722b5041d2d..d62f7873a58 100644 --- a/adapters/gamma/gammatest/supplemental/ignore-imp.json +++ b/adapters/gamma/gammatest/supplemental/ignore-imp.json @@ -114,36 +114,41 @@ } ], - "expectedBids": [ + "expectedBidResponses": [ { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["sample.com"], - "cid": "958", - "crid": "29681110", - "w": 1024, - "h": 576 - }, - "type": "banner" + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["sample.com"], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] }, { - "bid": { - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-video-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29484110", - "adomain": ["sample.com"], - "cid": "958", - "crid": "29484110", - "w": 1024, - "h": 576 - }, - "type": "video" + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "29484110", + "w": 640, + "h": 480 + }, + "type": "banner" + } + ] } ], "expectedMakeRequestsErrors": [ diff --git a/adapters/gamma/gammatest/supplemental/missing-adm.json b/adapters/gamma/gammatest/supplemental/missing-adm.json index 6b8ff64c04a..4e8603d3c9a 100644 --- a/adapters/gamma/gammatest/supplemental/missing-adm.json +++ b/adapters/gamma/gammatest/supplemental/missing-adm.json @@ -54,7 +54,7 @@ } } ], - "expectedBids": [ + "expectedBidResponses": [ { "bid": { "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", @@ -73,4 +73,4 @@ "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/gamma/params_test.go b/adapters/gamma/params_test.go index 56f1b591190..c6a149c23bc 100644 --- a/adapters/gamma/params_test.go +++ b/adapters/gamma/params_test.go @@ -8,8 +8,6 @@ import ( ) // This file actually intends to test static/bidder-params/gamma.json -// -// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.brightroll // TestValidParams makes sure that the Gamma schema accepts all imp.ext fields which we intend to support. func TestValidParams(t *testing.T) { diff --git a/adapters/gamoshi/gamoshi.go b/adapters/gamoshi/gamoshi.go index d130ce9639a..10c5127e613 100644 --- a/adapters/gamoshi/gamoshi.go +++ b/adapters/gamoshi/gamoshi.go @@ -6,7 +6,6 @@ import ( "net/http" "strconv" - "github.com/golang/glog" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" @@ -48,7 +47,6 @@ func (a *GamoshiAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada err := &errortypes.BadInput{ Message: fmt.Sprintf("Gamoshi only supports banner and video media types. Ignoring imp id=%s", request.Imp[i].ID), } - glog.Warning("Gamoshi SUPPORT VIOLATION: only banner and video media types supported") errs = append(errs, err) request.Imp = append(request.Imp[:i], request.Imp[i+1:]...) i-- diff --git a/adapters/gothamads/gothamads.go b/adapters/gothamads/gothamads.go new file mode 100644 index 00000000000..e486585c958 --- /dev/null +++ b/adapters/gothamads/gothamads.go @@ -0,0 +1,170 @@ +package gothamads + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + return bidder, nil +} + +func getHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("User-Agent", request.Device.UA) + } + + if len(request.Device.IPv6) > 0 { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + } + + return headers +} + +func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { + impExt, err := getImpressionExt(&openRTBRequest.Imp[0]) + if err != nil { + return nil, []error{err} + } + + openRTBRequest.Imp[0].Ext = nil + + url, err := a.buildEndpointURL(impExt) + if err != nil { + return nil, []error{err} + } + + reqJSON, err := json.Marshal(openRTBRequest) + if err != nil { + return nil, []error{err} + } + + return []*adapters.RequestData{{ + Method: http.MethodPost, + Body: reqJSON, + Uri: url, + Headers: getHeaders(openRTBRequest), + }}, nil +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtGothamAds, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + var gothamadsExt openrtb_ext.ExtGothamAds + if err := json.Unmarshal(bidderExt.Bidder, &gothamadsExt); err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + + return &gothamadsExt, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtGothamAds) (string, error) { + endpointParams := macros.EndpointTemplateParams{AccountID: params.AccountID} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func checkResponseStatusCodes(response *adapters.ResponseData) error { + if response.StatusCode == http.StatusServiceUnavailable { + return &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Something went wrong Status Code: [ %d ] ", response.StatusCode), + } + } + + return adapters.CheckResponseStatusCodeForErrors(response) +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + httpStatusError := checkResponseStatusCodes(bidderRawResponse) + if httpStatusError != nil { + return nil, []error{httpStatusError} + } + + responseBody := bidderRawResponse.Body + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bad Server Response", + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + var bidsArray []*adapters.TypedBid + + for _, sb := range bidResp.SeatBid { + for idx, bid := range sb.Bid { + bidType, err := getMediaTypeForImp(bid) + if err != nil { + return nil, []error{err} + } + + bidsArray = append(bidsArray, &adapters.TypedBid{ + Bid: &sb.Bid[idx], + BidType: bidType, + }) + } + } + + bidResponse.Bids = bidsArray + return bidResponse, nil +} + +func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("unsupported MType %d", bid.MType) + } +} diff --git a/adapters/gothamads/gothamads_test.go b/adapters/gothamads/gothamads_test.go new file mode 100644 index 00000000000..82e34dde1b3 --- /dev/null +++ b/adapters/gothamads/gothamads_test.go @@ -0,0 +1,28 @@ +package gothamads + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderGothamads, config.Adapter{ + Endpoint: "http://us-e-node1.gothamads.com/?pass={{.AccountID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "gothamadstest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderGothamads, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/gothamads/gothamadstest/exemplary/banner-app.json b/adapters/gothamads/gothamadstest/exemplary/banner-app.json new file mode 100644 index 00000000000..44966ea8888 --- /dev/null +++ b/adapters/gothamads/gothamadstest/exemplary/banner-app.json @@ -0,0 +1,151 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "gothamAds" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/exemplary/banner-web.json b/adapters/gothamads/gothamadstest/exemplary/banner-web.json new file mode 100644 index 00000000000..baac33a4f8c --- /dev/null +++ b/adapters/gothamads/gothamadstest/exemplary/banner-web.json @@ -0,0 +1,194 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "gothamads" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + }, + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/exemplary/native-app.json b/adapters/gothamads/gothamadstest/exemplary/native-app.json new file mode 100644 index 00000000000..5c54d526699 --- /dev/null +++ b/adapters/gothamads/gothamadstest/exemplary/native-app.json @@ -0,0 +1,147 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "type": "native", + "seat": "gothamads" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/exemplary/native-web.json b/adapters/gothamads/gothamadstest/exemplary/native-web.json new file mode 100644 index 00000000000..a2f16e78606 --- /dev/null +++ b/adapters/gothamads/gothamadstest/exemplary/native-web.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "2607:fb90:f27:4512:d800:cb23:a603:e245" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "seat": "gothamads" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/exemplary/video-app.json b/adapters/gothamads/gothamadstest/exemplary/video-app.json new file mode 100644 index 00000000000..a08c5a032c1 --- /dev/null +++ b/adapters/gothamads/gothamadstest/exemplary/video-app.json @@ -0,0 +1,160 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2 + } + ], + "seat": "gothamads" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 1280, + "h": 720, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/exemplary/video-web.json b/adapters/gothamads/gothamadstest/exemplary/video-web.json new file mode 100644 index 00000000000..64293e740ba --- /dev/null +++ b/adapters/gothamads/gothamadstest/exemplary/video-web.json @@ -0,0 +1,158 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + }, + "mtype": 2 + } + ], + "seat": "gothamads" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + }, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/bad_media_type.json b/adapters/gothamads/gothamadstest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..349ea5da3f4 --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/bad_media_type.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "test-imp-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + } + ], + "mtype": 0, + "seat": "gothamads" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unsupported MType 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/empty-seatbid-array.json b/adapters/gothamads/gothamadstest/supplemental/empty-seatbid-array.json new file mode 100644 index 00000000000..50eafd989af --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/empty-seatbid-array.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "mockResponse": { + "status": 200, + "body": "invalid response" + }, + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/invalid-bidder-ext-object.json b/adapters/gothamads/gothamadstest/supplemental/invalid-bidder-ext-object.json new file mode 100644 index 00000000000..e412d39ca2c --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/invalid-bidder-ext-object.json @@ -0,0 +1,33 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb_ext.ExtGothamAds", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": "bidderExt" + } + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/invalid-gotham-ext-object.json b/adapters/gothamads/gothamadstest/supplemental/invalid-gotham-ext-object.json new file mode 100644 index 00000000000..6075e2a4b3d --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/invalid-gotham-ext-object.json @@ -0,0 +1,31 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "wrongExt" + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/invalid-response.json b/adapters/gothamads/gothamadstest/supplemental/invalid-response.json new file mode 100644 index 00000000000..eb928b41551 --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/invalid-response.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bad Server Response", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-bad-request.json b/adapters/gothamads/gothamadstest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..37c047edaa3 --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/status-code-bad-request.json @@ -0,0 +1,92 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-no-content.json b/adapters/gothamads/gothamadstest/supplemental/status-code-no-content.json new file mode 100644 index 00000000000..76a3a4fc84d --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/status-code-no-content.json @@ -0,0 +1,75 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-other-error.json b/adapters/gothamads/gothamadstest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..45a0f67f3e0 --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/status-code-other-error.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 306 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 306. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-service-unavailable.json b/adapters/gothamads/gothamadstest/supplemental/status-code-service-unavailable.json new file mode 100644 index 00000000000..97773cd0d53 --- /dev/null +++ b/adapters/gothamads/gothamadstest/supplemental/status-code-service-unavailable.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://us-e-node1.gothamads.com/?pass=accountId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Something went wrong Status Code: [ 503 ] ", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gothamads/params_test.go b/adapters/gothamads/params_test.go new file mode 100644 index 00000000000..1290216d28e --- /dev/null +++ b/adapters/gothamads/params_test.go @@ -0,0 +1,50 @@ +package gothamads + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{"accountId": "hash"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderGothamads, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Gothamads params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"adCode": "string", "seatCode": 5, "originalPublisherid": "string"}`, + `{ "accountid": "" }`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderGothamads, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/huaweiads/huaweiads.go b/adapters/huaweiads/huaweiads.go index 28ae0d9ac2b..2b7c3c2548d 100644 --- a/adapters/huaweiads/huaweiads.go +++ b/adapters/huaweiads/huaweiads.go @@ -185,6 +185,7 @@ type metaData struct { ApkInfo apkInfo `json:"apkInfo"` Duration int64 `json:"duration"` MediaFile mediaFile `json:"mediaFile"` + Cta string `json:"cta"` } type imageInfo struct { @@ -535,8 +536,7 @@ func getNativeFormat(adslot30 *adslot30, openRTBImp *openrtb2.Imp) error { // only compute the main image number, type = native1.ImageAssetTypeMain var numMainImage = 0 var numVideo = 0 - var width int64 - var height int64 + for _, asset := range nativePayload.Assets { // Only one of the {title,img,video,data} objects should be present in each object. if asset.Video != nil { @@ -547,19 +547,10 @@ func getNativeFormat(adslot30 *adslot30, openRTBImp *openrtb2.Imp) error { if asset.Img != nil { if asset.Img.Type == native1.ImageAssetTypeMain { numMainImage++ - if asset.Img.H != 0 && asset.Img.W != 0 { - width = asset.Img.W - height = asset.Img.H - } else if asset.Img.WMin != 0 && asset.Img.HMin != 0 { - width = asset.Img.WMin - height = asset.Img.HMin - } } continue } } - adslot30.W = width - adslot30.H = height var detailedCreativeTypeList = make([]string, 0, 2) if numVideo >= 1 { @@ -723,6 +714,7 @@ func getReqDeviceInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidReq device.LocaleCountry = country device.Ip = openRTBRequest.Device.IP device.Gaid = openRTBRequest.Device.IFA + device.ClientTime = getClientTime("") } // get oaid gaid imei in openRTBRequest.User.Ext.Data @@ -771,7 +763,7 @@ func convertCountryCode(country string) (out string) { return mappedCountry } - if len(country) >= 3 { + if len(country) >= 2 { return country[0:2] } @@ -817,6 +809,9 @@ func getDeviceIDFromUserExt(device *device, openRTBRequest *openrtb2.BidRequest) device.Gaid = deviceId.Gaid[0] isValidDeviceId = true } + if len(device.Gaid) > 0 { + isValidDeviceId = true + } if len(deviceId.Imei) > 0 { device.Imei = deviceId.Imei[0] isValidDeviceId = true @@ -898,7 +893,6 @@ func getReqConsentInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRe if openRTBRequest.User != nil && openRTBRequest.User.Ext != nil { var extUser openrtb_ext.ExtUser if err := json.Unmarshal(openRTBRequest.User.Ext, &extUser); err != nil { - fmt.Errorf("failed to parse ExtUser in HuaweiAds GDPR check: %v", err) return } request.Consent = extUser.Consent @@ -1166,6 +1160,11 @@ func (a *adapter) extractAdmNative(adType int32, content *content, bidType openr dataObject.Label = "desc" dataObject.Value = getDecodeValue(content.MetaData.Description) } + + if asset.Data.Type == native1.DataAssetTypeCTAText { + dataObject.Type = native1.DataAssetTypeCTAText + dataObject.Value = getDecodeValue(content.MetaData.Cta) + } responseAsset.Data = &dataObject } var id = asset.ID @@ -1174,6 +1173,7 @@ func (a *adapter) extractAdmNative(adType int32, content *content, bidType openr } // dsp imp click tracking + imp click tracking + var eventTrackers []nativeResponse.EventTracker if content.Monitor != nil { for _, monitor := range content.Monitor { if len(monitor.Url) == 0 { @@ -1183,10 +1183,17 @@ func (a *adapter) extractAdmNative(adType int32, content *content, bidType openr linkObject.ClickTrackers = append(linkObject.ClickTrackers, monitor.Url...) } if monitor.EventType == "imp" { - nativeResult.ImpTrackers = append(nativeResult.ImpTrackers, monitor.Url...) + for i := range monitor.Url { + var eventTracker nativeResponse.EventTracker + eventTracker.Event = native1.EventTypeImpression + eventTracker.Method = native1.EventTrackingMethodImage + eventTracker.URL = monitor.Url[i] + eventTrackers = append(eventTrackers, eventTracker) + } } } } + nativeResult.EventTrackers = eventTrackers nativeResult.Link = linkObject nativeResult.Ver = "1.1" if nativePayload.Ver != "" { @@ -1552,10 +1559,14 @@ func getDigestAuthorization(huaweiAdsImpExt *openrtb_ext.ExtImpHuaweiAds, isTest if isTestAuthorization { nonce = "1629473330823" } - var apiKey = huaweiAdsImpExt.PublisherId + ":ppsadx/getResult:" + huaweiAdsImpExt.SignKey - return "Digest username=" + huaweiAdsImpExt.PublisherId + "," + + publisher_id := strings.TrimSpace(huaweiAdsImpExt.PublisherId) + sign_key := strings.TrimSpace(huaweiAdsImpExt.SignKey) + key_id := strings.TrimSpace(huaweiAdsImpExt.KeyId) + + var apiKey = publisher_id + ":ppsadx/getResult:" + sign_key + return "Digest username=" + publisher_id + "," + "realm=ppsadx/getResult," + "nonce=" + nonce + "," + "response=" + computeHmacSha256(nonce+":POST:/ppsadx/getResult", apiKey) + "," + - "algorithm=HmacSHA256,usertype=1,keyid=" + huaweiAdsImpExt.KeyId + "algorithm=HmacSHA256,usertype=1,keyid=" + key_id } diff --git a/adapters/huaweiads/huaweiadstest/exemplary/banner1_without_userext.json b/adapters/huaweiads/huaweiadstest/exemplary/banner1_without_userext.json new file mode 100644 index 00000000000..34b8b69e293 --- /dev/null +++ b/adapters/huaweiads/huaweiadstest/exemplary/banner1_without_userext.json @@ -0,0 +1,251 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "slotid": "m8x9x3rzff", + "adtype": "banner", + "publisherid": "123 ", + "signkey": "signkey ", + "keyid": "41", + "isTestAuthorization": "true" + } + } + } + ], + "app": { + "bundle": "com.example.p1", + "name": "Huawei Browser", + "ver": "9.1.0.301", + "content": { + "language": "zh" + } + }, + "device": { + "ua": "useragent", + "h": 1920, + "language": "zh", + "model": "COL-TEST", + "os": "android", + "osv": "10.0.0", + "devicetype": 4, + "make": "huawei", + "w": 1080, + "ip": "ip", + "ifa": "e4fe9bde-caa0-47b6-908d-ffba3fa184f2", + "pxratio": 23.01, + "mccmnc": "460", + "geo": { + "country": "CHN" + } + }, + "user": { + "ext": { + "data": { + "clientTime": [ + "2018-11-02 16:34:07.981+1300" + ] + }, + "consent": "CPaYLJBPaYLJBIPAAAENCSCgAPAAAAAAAAAAGsQAQGsAAAAA.YAAAAAAAAAA" + } + }, + "regs": { + "coppa": 1 + }, + "ext": { + } + }, + "httpcalls": [ + { + "expectedRequest": { + "uri": "https://acd.op.hicloud.com/ppsadx/getResult", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "User-Agent": [ + "useragent" + ], + "Authorization": [ + "Digest username=123,realm=ppsadx/getResult,nonce=1629473330823,response=d1d61a13a83e1468aa4dff5c8a6cee0b8b381173ca3eb6fa9b313937684d87c0,algorithm=HmacSHA256,usertype=1,keyid=41" + ] + }, + "body": { + "app": { + "lang": "zh", + "country": "CN", + "name": "Huawei Browser", + "pkgname": "com.example.pkgname1", + "version": "9.1.0.301" + }, + "multislot": [ + { + "adtype": 8, + "slotid": "m8x9x3rzff", + "test": 1, + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + } + ], + "device": { + "height": 1920, + "language": "zh", + "os": "android", + "type": 4, + "ip": "ip", + "localeCountry": "CN", + "pxratio": 23.01, + "width": 1080, + "clientTime": "2018-11-02 16:34:07.981+1300", + "gaid": "e4fe9bde-caa0-47b6-908d-ffba3fa184f2", + "useragent": "useragent", + "version": "10.0.0", + "maker": "huawei", + "model": "COL-TEST", + "belongCountry": "CN" + }, + "geo": { + }, + "network": { + "type": 0 + }, + "regs": { + "coppa": 1 + }, + "consent": "CPaYLJBPaYLJBIPAAAENCSCgAPAAAAAAAAAAGsQAQGsAAAAA.YAAAAAAAAAA", + "version": "3.4", + "clientAdRequestId": "test-req-id" + } + }, + "mockResponse": { + "status": 200, + "body": { + "ctrlSwitchs": "0", + "dsp1cost": 61, + "dspcost": 108, + "multiad": [ + { + "adtype": 8, + "brsetting": "Y", + "content": [ + { + "clickActionList": [ + 1 + ], + "contentid": "58025103", + "creativetype": 2, + "ctrlSwitchs": "001011001001010112", + "endtime": 1621428898335, + "filterList": [ + 3 + ], + "interactiontype": 1, + "landingTitle": 1, + "metaData": { + "adSign": "2", + "appPromotionChannel": "401721412", + "clickUrl": "https://ads.huawei.com/usermgtportal/home/index.html#/", + "imageInfo": [ + { + "checkSha256Flag": 1, + "height": 300, + "imageType": "img", + "sha256": "f5d8487cddaecec45b73fc078649478c80ec646bfec8ed7da4ff931f90eab232", + "url": "https://ads.huawei.com/usermgtportal/home/img/huawei_logo_black.aaec817d.svg", + "width": 250 + } + ], + "label": "Banner_API", + "landingPageType": "3" + }, + "monitor": [ + { + "eventType": "click", + "url": [ + "http://test/click1", + "http://test/click2", + "http://test/click3" + ] + }, + { + "eventType": "imp", + "url": [ + "http://test/imp1", + "http://test/imp2", + "http://test/imp3" + ] + } + ], + "paramfromserver": { + "a": "1||test", + "sig": "", + "t": "99990101235959" + }, + "price": 2.8, + "starttime": 1621267200000, + "taskid": "48017658" + } + ], + "retcode30": 200, + "slotid": "m8x9x3rzff" + } + ], + "noReportAdTypeEventList": [ + { + "adType": 8, + "eventTypeList": [ + "exception_6" + ] + } + ], + "retcode": 200, + "totalCacheSize": 300 + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": " ", + "adomain": [ + "huaweiads" + ], + "crid": "58025103", + "id": "test-imp-id", + "impid": "test-imp-id", + "price": 2.8, + "h": 300, + "w": 250 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/huaweiads/huaweiadstest/exemplary/banner2.json b/adapters/huaweiads/huaweiadstest/exemplary/banner2.json index 4f1cc6b498a..92afb558743 100644 --- a/adapters/huaweiads/huaweiadstest/exemplary/banner2.json +++ b/adapters/huaweiads/huaweiadstest/exemplary/banner2.json @@ -51,7 +51,7 @@ "carrier": "carrier", "connectiontype": 1, "geo": { - "country": "ZAF" + "country": "ZA" } }, "app": { @@ -269,4 +269,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeIncludeVideo.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeIncludeVideo.json index 8297d11b824..24aad066b3d 100644 --- a/adapters/huaweiads/huaweiadstest/exemplary/nativeIncludeVideo.json +++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeIncludeVideo.json @@ -95,9 +95,7 @@ "detailedCreativeTypeList": [ "903" ], - "h": 200, - "test": 1, - "w": 200 + "test": 1 } ], "device": { @@ -312,7 +310,7 @@ "huaweiads" ], "crid": "58022259", - "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":101,\"img\":{\"type\":3,\"url\":\"http://image1.jpg\",\"w\":400,\"h\":350}},{\"id\":107,\"video\":{\"vasttag\":\"HuaweiAds/test/00:00:06.038 \"}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"this is a test ad\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\",\"http://test/dspclick\"]},\"imptrackers\":[\"http://test/imp\",\"http://test/dspimp\"]}", + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":101,\"img\":{\"type\":3,\"url\":\"http://image1.jpg\",\"w\":400,\"h\":350}},{\"id\":107,\"video\":{\"vasttag\":\"HuaweiAds/test/00:00:06.038 \"}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"this is a test ad\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\",\"http://test/dspclick\"]},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://test/imp\"},{\"event\":1,\"method\":1,\"url\":\"http://test/dspimp\"}]}", "id": "test-imp-id", "impid": "test-imp-id", "price": 2.8, diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json index 1af8abc5c0a..8e86675b4a6 100644 --- a/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json +++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json @@ -5,7 +5,7 @@ { "id": "test-imp-id", "native": { - "request": "{\"context\":2,\"contextsubtype\":20,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"aurlsupport\":0,\"durlsupport\":0,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]}],\"privacy\":0,\"assets\":[{\"id\":100,\"title\":{\"len\":90},\"required\":1},{\"id\":103,\"img\":{\"type\":3,\"wmin\":200,\"hmin\":200},\"required\":1},{\"id\":105,\"data\":{\"type\":2,\"len\":90},\"required\":1}],\"ver\":\"1.2\"}", + "request": "{\"context\":2,\"contextsubtype\":20,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"aurlsupport\":0,\"durlsupport\":0,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]}],\"privacy\":0,\"assets\":[{\"id\":101,\"title\":{\"len\":90},\"required\":1},{\"id\":102,\"img\":{\"type\":3,\"wmin\":200,\"hmin\":200},\"required\":1},{\"id\":103,\"data\":{\"type\":2,\"len\":90},\"required\":1},{\"id\":105,\"data\":{\"type\":12,\"len\":90}}],\"ver\":\"1.2\"}", "ver": "1.2" }, "ext": { @@ -93,9 +93,7 @@ "detailedCreativeTypeList": [ "901" ], - "h": 200, - "test": 1, - "w": 200 + "test": 1 } ], "device": { @@ -166,6 +164,7 @@ "appId": "101219405", "appPromotionChannel": "401721412", "clickUrl": "https://ads.huawei.com/usermgtportal/home/index.html#/", + "cta": "install", "duration": 6038, "description": "", "icon": [ @@ -300,7 +299,7 @@ "huaweiads" ], "crid": "58022259", - "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":103,\"img\":{\"type\":3,\"url\":\"http://image.jpg\",\"w\":720,\"h\":1280}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"imptrackers\":[\"http://test/imp\"]}", + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":101,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":102,\"img\":{\"type\":3,\"url\":\"http://image.jpg\",\"w\":720,\"h\":1280}},{\"id\":103,\"data\":{\"label\":\"desc\",\"value\":\"\"}},{\"id\":105,\"data\":{\"type\":12,\"value\":\"install\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://test/imp\"}]}", "id": "test-imp-id", "impid": "test-imp-id", "price": 2.8, diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImage.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImage.json index 7b5c9a54f44..c29b2352d59 100644 --- a/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImage.json +++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImage.json @@ -92,9 +92,7 @@ "detailedCreativeTypeList": [ "904" ], - "h": 200, - "test": 1, - "w": 200 + "test": 1 } ], "device": { @@ -319,7 +317,7 @@ "huaweiads" ], "crid": "58022259", - "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":101,\"img\":{\"type\":3,\"url\":\"http://image1.jpg\",\"w\":400,\"h\":350}},{\"id\":102,\"img\":{\"type\":3,\"url\":\"http://image2.jpg\",\"w\":400,\"h\":300}},{\"id\":103,\"img\":{\"type\":3,\"url\":\"http://image3.jpg\",\"w\":400,\"h\":300}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"this is a test ad\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"imptrackers\":[\"http://test/imp\"]}", + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":101,\"img\":{\"type\":3,\"url\":\"http://image1.jpg\",\"w\":400,\"h\":350}},{\"id\":102,\"img\":{\"type\":3,\"url\":\"http://image2.jpg\",\"w\":400,\"h\":300}},{\"id\":103,\"img\":{\"type\":3,\"url\":\"http://image3.jpg\",\"w\":400,\"h\":300}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"this is a test ad\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://test/imp\"}]}", "id": "test-imp-id", "impid": "test-imp-id", "price": 2.8, diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json index e72f5f593c1..5d3af50621a 100644 --- a/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json +++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json @@ -93,9 +93,7 @@ "detailedCreativeTypeList": [ "904" ], - "h": 200, - "test": 1, - "w": 200 + "test": 1 } ], "device": { @@ -320,7 +318,7 @@ "huaweiads" ], "crid": "58022259", - "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":101,\"img\":{\"type\":3,\"url\":\"http://image1.jpg\",\"w\":400,\"h\":350}},{\"id\":102,\"img\":{\"type\":1,\"url\":\"https://icon1.png\",\"w\":160,\"h\":160}},{\"id\":103,\"img\":{\"type\":3,\"url\":\"http://image2.jpg\",\"w\":400,\"h\":300}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"this is a test ad\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"imptrackers\":[\"http://test/imp\"]}", + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":100,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":101,\"img\":{\"type\":3,\"url\":\"http://image1.jpg\",\"w\":400,\"h\":350}},{\"id\":102,\"img\":{\"type\":1,\"url\":\"https://icon1.png\",\"w\":160,\"h\":160}},{\"id\":103,\"img\":{\"type\":3,\"url\":\"http://image2.jpg\",\"w\":400,\"h\":300}},{\"id\":105,\"data\":{\"label\":\"desc\",\"value\":\"this is a test ad\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://test/imp\"}]}", "id": "test-imp-id", "impid": "test-imp-id", "price": 2.8, diff --git a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_dont_find_impid.json b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_dont_find_impid.json index e94e5fb716d..f040ce955be 100644 --- a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_dont_find_impid.json +++ b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_dont_find_impid.json @@ -319,7 +319,27 @@ } ], "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "adm": " ", + "adomain": [ + "huaweiads" + ], + "crid": "58025103", + "id": "test-imp-id", + "impid": "test-imp-id", + "price": 2.8, + "h": 300, + "w": 250 + }, + "type": "banner" + } + ] + } ], "expectedMakeRequestsErrors": [], "expectedMakeBidsErrors": [] -} \ No newline at end of file +} diff --git a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json index c38bc9f69b9..1466737008f 100644 --- a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json +++ b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json @@ -93,9 +93,7 @@ "test": 1, "detailedCreativeTypeList": [ "903" - ], - "h": 200, - "w": 200 + ] } ], "device": { diff --git a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_retcode30_204.json b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_retcode30_204.json index 466209d967e..d4a41bba3fc 100644 --- a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_retcode30_204.json +++ b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_retcode30_204.json @@ -155,9 +155,8 @@ } } ], - "expectedBidResponses": [ - ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeRequestsErrors": [], "expectedMakeBidsErrors": [ ] -} \ No newline at end of file +} diff --git a/adapters/imds/imds.go b/adapters/imds/imds.go index 6c555bcca45..02d30828966 100644 --- a/adapters/imds/imds.go +++ b/adapters/imds/imds.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "text/template" "github.com/prebid/openrtb/v19/openrtb2" @@ -14,6 +15,8 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) +const adapterVersion string = "pbs-go/1.0.0" + type adapter struct { EndpointTemplate *template.Template } @@ -127,7 +130,7 @@ func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestDa // Builds enpoint url based on adapter-specific pub settings from imp.ext func (adapter *adapter) buildEndpointURL(params *openrtb_ext.ExtImpImds) (string, error) { - return macros.ResolveMacros(adapter.EndpointTemplate, macros.EndpointTemplateParams{Host: params.SeatId}) + return macros.ResolveMacros(adapter.EndpointTemplate, macros.EndpointTemplateParams{AccountID: url.QueryEscape(params.SeatId), SourceId: url.QueryEscape(adapterVersion)}) } func getExtImpObj(imp *openrtb2.Imp) (*openrtb_ext.ExtImpImds, error) { diff --git a/adapters/imds/imds_test.go b/adapters/imds/imds_test.go index 3a8c33ed31c..5fab0509c1d 100644 --- a/adapters/imds/imds_test.go +++ b/adapters/imds/imds_test.go @@ -11,7 +11,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderImds, config.Adapter{ - Endpoint: "http://{{.Host}}.technoratimedia.com/openrtb/bids/{{.Host}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "http://pbs.technoratimedia.com/openrtb/bids/{{.AccountID}}?src={{.SourceId}}&adapter=imds"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { t.Fatalf("Builder returned unexpected error %v", buildErr) diff --git a/adapters/imds/imdstest/exemplary/simple-banner.json b/adapters/imds/imdstest/exemplary/simple-banner.json index 5b15f9e058a..c19d19bd5fe 100644 --- a/adapters/imds/imdstest/exemplary/simple-banner.json +++ b/adapters/imds/imdstest/exemplary/simple-banner.json @@ -25,7 +25,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/exemplary/simple-video.json b/adapters/imds/imdstest/exemplary/simple-video.json index 9aaea18cca9..31a8d568b5d 100644 --- a/adapters/imds/imdstest/exemplary/simple-video.json +++ b/adapters/imds/imdstest/exemplary/simple-video.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "1", "site": { diff --git a/adapters/imds/imdstest/supplemental/audio_response.json b/adapters/imds/imdstest/supplemental/audio_response.json index 752d610aa72..9873d9b91d4 100644 --- a/adapters/imds/imdstest/supplemental/audio_response.json +++ b/adapters/imds/imdstest/supplemental/audio_response.json @@ -20,7 +20,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/supplemental/bad_response.json b/adapters/imds/imdstest/supplemental/bad_response.json index 520f415bcf4..1f96ac643bb 100644 --- a/adapters/imds/imdstest/supplemental/bad_response.json +++ b/adapters/imds/imdstest/supplemental/bad_response.json @@ -29,7 +29,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/supplemental/native_response.json b/adapters/imds/imdstest/supplemental/native_response.json index 1428ac1ccd3..b402084a97e 100644 --- a/adapters/imds/imdstest/supplemental/native_response.json +++ b/adapters/imds/imdstest/supplemental/native_response.json @@ -20,7 +20,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/supplemental/one_bad_ext.json b/adapters/imds/imdstest/supplemental/one_bad_ext.json index f5e9fed874d..d1bd7451384 100644 --- a/adapters/imds/imdstest/supplemental/one_bad_ext.json +++ b/adapters/imds/imdstest/supplemental/one_bad_ext.json @@ -42,7 +42,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/supplemental/status_204.json b/adapters/imds/imdstest/supplemental/status_204.json index 302e8ed1585..77906a7b54b 100644 --- a/adapters/imds/imdstest/supplemental/status_204.json +++ b/adapters/imds/imdstest/supplemental/status_204.json @@ -29,7 +29,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/supplemental/status_400.json b/adapters/imds/imdstest/supplemental/status_400.json index 1bb2cf6fa45..9a63221972b 100644 --- a/adapters/imds/imdstest/supplemental/status_400.json +++ b/adapters/imds/imdstest/supplemental/status_400.json @@ -29,7 +29,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/imds/imdstest/supplemental/status_500.json b/adapters/imds/imdstest/supplemental/status_500.json index 37ca398e59e..ee29aee4329 100644 --- a/adapters/imds/imdstest/supplemental/status_500.json +++ b/adapters/imds/imdstest/supplemental/status_500.json @@ -29,7 +29,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "uri": "http://pbs.technoratimedia.com/openrtb/bids/prebid?src=pbs-go%2F1.0.0&adapter=imds", "body": { "id": "test-request-id", "ext": { diff --git a/adapters/impactify/impactifytest/supplemental/http_204.json b/adapters/impactify/impactifytest/supplemental/http_204.json index bca38ba06ac..d3f790b21d2 100644 --- a/adapters/impactify/impactifytest/supplemental/http_204.json +++ b/adapters/impactify/impactifytest/supplemental/http_204.json @@ -58,11 +58,5 @@ } } } - ], - - "expectedBidResponses": [ - { - "bids": [] - } ] } diff --git a/adapters/impactify/impactifytest/supplemental/no_seat_bid.json b/adapters/impactify/impactifytest/supplemental/no_seat_bid.json index e1371c3ad20..21a3d00ec18 100644 --- a/adapters/impactify/impactifytest/supplemental/no_seat_bid.json +++ b/adapters/impactify/impactifytest/supplemental/no_seat_bid.json @@ -58,11 +58,5 @@ } } } - ], - - "expectedBidResponses": [ - { - "bids": [] - } ] } diff --git a/adapters/improvedigital/improvedigital.go b/adapters/improvedigital/improvedigital.go index 4c64451f247..b934ac753a0 100644 --- a/adapters/improvedigital/improvedigital.go +++ b/adapters/improvedigital/improvedigital.go @@ -140,6 +140,7 @@ func (a *ImprovedigitalAdapter) MakeBids(internalRequest *openrtb2.BidRequest, e } bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seatBid.Bid)) + bidResponse.Currency = bidResp.Cur for i := range seatBid.Bid { bid := seatBid.Bid[i] diff --git a/adapters/improvedigital/improvedigitaltest/supplemental/foreign-currency.json b/adapters/improvedigital/improvedigitaltest/supplemental/foreign-currency.json new file mode 100644 index 00000000000..633e8b6b0aa --- /dev/null +++ b/adapters/improvedigital/improvedigitaltest/supplemental/foreign-currency.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "cur": ["EUR"], + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "placementId": 13245 + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/pbs", + "body": { + "id": "test-request-id", + "cur": ["EUR"], + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "placementId": 13245 + } + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "EUR", + "seatbid": [{ + "seat": "improvedigital", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + }] + }] + } + } + }], + + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/improvedigital/improvedigitaltest/supplemental/rewarded-inventory.json b/adapters/improvedigital/improvedigitaltest/supplemental/rewarded-inventory.json index 44921fe98f7..17c8a656b89 100644 --- a/adapters/improvedigital/improvedigitaltest/supplemental/rewarded-inventory.json +++ b/adapters/improvedigital/improvedigitaltest/supplemental/rewarded-inventory.json @@ -170,8 +170,5 @@ "body": {} } } - ], - "expectedBidResponses": [ - {},{},{} ] } diff --git a/adapters/invibes/invibestest/amp/amp-ad.json b/adapters/invibes/invibestest/amp/amp-ad.json index 63a037b72c1..742df1bba65 100644 --- a/adapters/invibes/invibestest/amp/amp-ad.json +++ b/adapters/invibes/invibestest/amp/amp-ad.json @@ -73,5 +73,23 @@ } } ], - "expectedBidResponses": [] -} \ No newline at end of file + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "1234", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
abc
", + "crid": "29681110", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/invibes/invibestest/exemplary/no-ad.json b/adapters/invibes/invibestest/exemplary/no-ad.json index 4f7718c90dc..56be0abdbd7 100644 --- a/adapters/invibes/invibestest/exemplary/no-ad.json +++ b/adapters/invibes/invibestest/exemplary/no-ad.json @@ -60,5 +60,5 @@ } } ], - "expectedBidResponses": [] -} \ No newline at end of file + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/invibes/invibestest/exemplary/test-ad.json b/adapters/invibes/invibestest/exemplary/test-ad.json index fb4d58c17d8..44160b923c0 100644 --- a/adapters/invibes/invibestest/exemplary/test-ad.json +++ b/adapters/invibes/invibestest/exemplary/test-ad.json @@ -73,5 +73,23 @@ } } ], - "expectedBidResponses": [] -} \ No newline at end of file + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "1234", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
abc
", + "crid": "29681110", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ix.go b/adapters/ix/ix.go index b17d913f42d..300858e3205 100644 --- a/adapters/ix/ix.go +++ b/adapters/ix/ix.go @@ -19,8 +19,7 @@ import ( ) type IxAdapter struct { - URI string - maxRequests int + URI string } type ExtRequest struct { @@ -30,108 +29,142 @@ type ExtRequest struct { } type IxDiag struct { - PbsV string `json:"pbsv,omitempty"` - PbjsV string `json:"pbjsv,omitempty"` + PbsV string `json:"pbsv,omitempty"` + PbjsV string `json:"pbjsv,omitempty"` + MultipleSiteIds string `json:"multipleSiteIds,omitempty"` } func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - nImp := len(request.Imp) - if nImp > a.maxRequests { - request.Imp = request.Imp[:a.maxRequests] - nImp = a.maxRequests - } - + requests := make([]*adapters.RequestData, 0, len(request.Imp)) errs := make([]error, 0) - if err := BuildIxDiag(request); err != nil { - errs = append(errs, err) - } - - // Multi-size banner imps are split into single-size requests. - // The first size imp requests are added to the first slice. - // Additional size requests are added to the second slice and are merged with the first at the end. - // Preallocate the max possible size to avoid reallocating arrays. - requests := make([]*adapters.RequestData, 0, a.maxRequests) - multiSizeRequests := make([]*adapters.RequestData, 0, a.maxRequests-nImp) - headers := http.Header{ "Content-Type": {"application/json;charset=utf-8"}, "Accept": {"application/json"}} - imps := request.Imp - for iImp := range imps { - request.Imp = imps[iImp : iImp+1] - if request.Site != nil { - if err := setSitePublisherId(request, iImp); err != nil { - errs = append(errs, err) - continue - } + uniqueSiteIDs := make(map[string]struct{}) + filteredImps := make([]openrtb2.Imp, 0, len(request.Imp)) + requestCopy := *request + + ixDiag := &IxDiag{} + + for _, imp := range requestCopy.Imp { + var err error + ixExt, err := unmarshalToIxExt(&imp) + + if err != nil { + errs = append(errs, err) + continue } - if request.Imp[0].Banner != nil { - banner := *request.Imp[0].Banner - request.Imp[0].Banner = &banner - formats := getBannerFormats(&banner) - for iFmt := range formats { - banner.Format = formats[iFmt : iFmt+1] - banner.W = openrtb2.Int64Ptr(banner.Format[0].W) - banner.H = openrtb2.Int64Ptr(banner.Format[0].H) - if requestData, err := createRequestData(a, request, &headers); err == nil { - if iFmt == 0 { - requests = append(requests, requestData) - } else { - multiSizeRequests = append(multiSizeRequests, requestData) - } - } else { - errs = append(errs, err) - } - if len(multiSizeRequests) == cap(multiSizeRequests) { - break - } + if err = parseSiteId(ixExt, uniqueSiteIDs); err != nil { + errs = append(errs, err) + continue + } + + if err := moveSid(&imp, ixExt); err != nil { + errs = append(errs, err) + } + + if imp.Banner != nil { + bannerCopy := *imp.Banner + + if len(bannerCopy.Format) == 0 && bannerCopy.W != nil && bannerCopy.H != nil { + bannerCopy.Format = []openrtb2.Format{{W: *bannerCopy.W, H: *bannerCopy.H}} + } + + if len(bannerCopy.Format) == 1 { + bannerCopy.W = openrtb2.Int64Ptr(bannerCopy.Format[0].W) + bannerCopy.H = openrtb2.Int64Ptr(bannerCopy.Format[0].H) } - } else if requestData, err := createRequestData(a, request, &headers); err == nil { + imp.Banner = &bannerCopy + } + filteredImps = append(filteredImps, imp) + } + requestCopy.Imp = filteredImps + + setPublisherId(&requestCopy, uniqueSiteIDs, ixDiag) + + err := setIxDiagIntoExtRequest(&requestCopy, ixDiag) + if err != nil { + errs = append(errs, err) + } + + if len(requestCopy.Imp) != 0 { + if requestData, err := createRequestData(a, &requestCopy, &headers); err == nil { requests = append(requests, requestData) } else { errs = append(errs, err) } } - request.Imp = imps - return append(requests, multiSizeRequests...), errs + return requests, errs } -func setSitePublisherId(request *openrtb2.BidRequest, iImp int) error { - if iImp == 0 { - // first impression - create a site and pub copy - site := *request.Site +func setPublisherId(requestCopy *openrtb2.BidRequest, uniqueSiteIDs map[string]struct{}, ixDiag *IxDiag) { + siteIDs := make([]string, 0, len(uniqueSiteIDs)) + for key := range uniqueSiteIDs { + siteIDs = append(siteIDs, key) + } + if requestCopy.Site != nil { + site := *requestCopy.Site if site.Publisher == nil { site.Publisher = &openrtb2.Publisher{} } else { publisher := *site.Publisher site.Publisher = &publisher } - request.Site = &site + if len(siteIDs) == 1 { + site.Publisher.ID = siteIDs[0] + } + requestCopy.Site = &site + } + + if requestCopy.App != nil { + app := *requestCopy.App + + if app.Publisher == nil { + app.Publisher = &openrtb2.Publisher{} + } else { + publisher := *app.Publisher + app.Publisher = &publisher + } + if len(siteIDs) == 1 { + app.Publisher.ID = siteIDs[0] + } + requestCopy.App = &app + } + + if len(siteIDs) > 1 { + // Sorting siteIDs for predictable output as Go maps don't guarantee order + sort.Strings(siteIDs) + multipleSiteIDs := strings.Join(siteIDs, ", ") + ixDiag.MultipleSiteIds = multipleSiteIDs } +} +func unmarshalToIxExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpIx, error) { var bidderExt adapters.ExtImpBidder - if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil { - return err + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, err } var ixExt openrtb_ext.ExtImpIx if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { - return err + return nil, err } - request.Site.Publisher.ID = ixExt.SiteId - return nil + return &ixExt, nil } -func getBannerFormats(banner *openrtb2.Banner) []openrtb2.Format { - if len(banner.Format) == 0 && banner.W != nil && banner.H != nil { - banner.Format = []openrtb2.Format{{W: *banner.W, H: *banner.H}} +func parseSiteId(ixExt *openrtb_ext.ExtImpIx, uniqueSiteIDs map[string]struct{}) error { + if ixExt == nil { + return fmt.Errorf("Nil Ix Ext") + } + if ixExt.SiteId != "" { + uniqueSiteIDs[ixExt.SiteId] = struct{}{} } - return banner.Format + return nil } func createRequestData(a *IxAdapter, request *openrtb2.BidRequest, headers *http.Header) (*adapters.RequestData, error) { @@ -180,7 +213,8 @@ func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalReque } } - bidderResponse := adapters.NewBidderResponseWithBidsCapacity(5) + // capacity 0 will make channel unbuffered + bidderResponse := adapters.NewBidderResponseWithBidsCapacity(0) bidderResponse.Currency = bidResponse.Cur var errs []error @@ -275,8 +309,7 @@ func getMediaTypeForBid(bid openrtb2.Bid, impMediaTypeReq map[string]openrtb_ext // Builder builds a new instance of the Ix adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &IxAdapter{ - URI: config.Endpoint, - maxRequests: 20, + URI: config.Endpoint, } return bidder, nil } @@ -328,29 +361,33 @@ func marshalJsonWithoutUnicode(v interface{}) (string, error) { return strings.TrimSuffix(sb.String(), "\n"), nil } -func BuildIxDiag(request *openrtb2.BidRequest) error { +func setIxDiagIntoExtRequest(request *openrtb2.BidRequest, ixDiag *IxDiag) error { extRequest := &ExtRequest{} if request.Ext != nil { if err := json.Unmarshal(request.Ext, &extRequest); err != nil { return err } } - ixdiag := &IxDiag{} if extRequest.Prebid != nil && extRequest.Prebid.Channel != nil { - ixdiag.PbjsV = extRequest.Prebid.Channel.Version + ixDiag.PbjsV = extRequest.Prebid.Channel.Version } - // Slice commit hash out of version if strings.Contains(version.Ver, "-") { - ixdiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")] + ixDiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")] } else if version.Ver != "" { - ixdiag.PbsV = version.Ver + ixDiag.PbsV = version.Ver } // Only set request.ext if ixDiag is not empty - if *ixdiag != (IxDiag{}) { - extRequest.IxDiag = ixdiag + if *ixDiag != (IxDiag{}) { + extRequest := &ExtRequest{} + if request.Ext != nil { + if err := json.Unmarshal(request.Ext, &extRequest); err != nil { + return err + } + } + extRequest.IxDiag = ixDiag extRequestJson, err := json.Marshal(extRequest) if err != nil { return err @@ -359,3 +396,24 @@ func BuildIxDiag(request *openrtb2.BidRequest) error { } return nil } + +// moves sid from imp[].ext.bidder.sid to imp[].ext.sid +func moveSid(imp *openrtb2.Imp, ixExt *openrtb_ext.ExtImpIx) error { + if ixExt == nil { + return fmt.Errorf("Nil Ix Ext") + } + + if ixExt.Sid != "" { + var m map[string]interface{} + if err := json.Unmarshal(imp.Ext, &m); err != nil { + return err + } + m["sid"] = ixExt.Sid + ext, err := json.Marshal(m) + if err != nil { + return err + } + imp.Ext = ext + } + return nil +} diff --git a/adapters/ix/ix_test.go b/adapters/ix/ix_test.go index 0f6b856dce4..a64e3d0c661 100644 --- a/adapters/ix/ix_test.go +++ b/adapters/ix/ix_test.go @@ -19,8 +19,6 @@ const endpoint string = "http://host/endpoint" func TestJsonSamples(t *testing.T) { if bidder, err := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}); err == nil { - ixBidder := bidder.(*IxAdapter) - ixBidder.maxRequests = 2 adapterstest.RunJSONBidderTest(t, "ixtest", bidder) } else { t.Fatalf("Builder returned unexpected error %v", err) @@ -44,7 +42,7 @@ func TestIxMakeBidsWithCategoryDuration(t *testing.T) { `{ "prebid": {}, "bidder": { - "siteID": 123456 + "siteID": "123456" } }`, )}, @@ -106,7 +104,6 @@ func TestIxMakeBidsWithCategoryDuration(t *testing.T) { func TestIxMakeRequestWithGppString(t *testing.T) { bidder := &IxAdapter{} - bidder.maxRequests = 2 testGppString := "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN" @@ -124,7 +121,7 @@ func TestIxMakeRequestWithGppString(t *testing.T) { `{ "prebid": {}, "bidder": { - "siteID": 123456 + "siteId": "123456" } }`, )}, @@ -256,7 +253,8 @@ func TestBuildIxDiag(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { version.Ver = test.pbsVersion - err := BuildIxDiag(test.request) + ixDiag := &IxDiag{} + err := setIxDiagIntoExtRequest(test.request, ixDiag) if test.expectError { assert.NotNil(t, err) } else { diff --git a/adapters/ix/ixtest/exemplary/app-site-id.json b/adapters/ix/ixtest/exemplary/app-site-id.json new file mode 100644 index 00000000000..80441cc4382 --- /dev/null +++ b/adapters/ix/ixtest/exemplary/app-site-id.json @@ -0,0 +1,140 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "app": { + "domain": "https://www.example.com/" + }, + "ext": { + "prebid": { + "channel": { + "name": "web", + "version": "7.0.0" + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "app": { + "domain": "https://www.example.com/", + "publisher": { + "id": "569749" + } + }, + "ext": { + "ixdiag": { + "pbjsv": "7.0.0" + }, + "prebid": { + "channel": { + "name": "web", + "version": "7.0.0" + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/multi-imp-multi-size-requests.json b/adapters/ix/ixtest/exemplary/multi-imp-multi-size-requests.json new file mode 100644 index 00000000000..4decdaf985d --- /dev/null +++ b/adapters/ix/ixtest/exemplary/multi-imp-multi-size-requests.json @@ -0,0 +1,211 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 600 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + "id": "569749" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 600, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 600, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/max-requests.json b/adapters/ix/ixtest/exemplary/multi-imp-requests.json similarity index 69% rename from adapters/ix/ixtest/exemplary/max-requests.json rename to adapters/ix/ixtest/exemplary/multi-imp-requests.json index 46d9ec5d6b7..d8d00aeea69 100644 --- a/adapters/ix/ixtest/exemplary/max-requests.json +++ b/adapters/ix/ixtest/exemplary/multi-imp-requests.json @@ -9,10 +9,6 @@ { "w": 300, "h": 250 - }, - { - "w": 300, - "h": 600 } ] }, @@ -62,6 +58,38 @@ "siteId": "569749" } } + }, + { + "id": "test-imp-id-4", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-5", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } } ] }, @@ -89,49 +117,7 @@ "siteId": "569749" } } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [ - { - "id": "7706636740145184841", - "impid": "test-imp-id-1", - "price": 0.5, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": [ - "https://advertiser.example.com" - ], - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300, - "ext": { - "ix": {} - } - } - ] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - }, - { - "expectedRequest": { - "uri": "http://host/endpoint", - "body": { - "id": "test-request-id", - "imp": [ + }, { "id": "test-imp-id-2", "video": { @@ -156,6 +142,60 @@ "siteId": "569749" } } + }, + { + "banner": { + "format": [ + { + "h": 600, + "w": 300 + } + ], + "h": 600, + "w": 300 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + }, + "id": "test-imp-id-3" + }, + { + "id": "test-imp-id-4", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-5", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } } ] } @@ -170,7 +210,7 @@ "bid": [ { "id": "7706636740145184841", - "impid": "test-imp-id-2", + "impid": "test-imp-id-1", "price": 0.5, "adid": "29681110", "adm": "some-test-ad", @@ -181,9 +221,6 @@ "crid": "29681110", "h": 250, "w": 300, - "cat": [ - "IAB9-1" - ], "ext": { "ix": {} } @@ -222,34 +259,6 @@ "type": "banner" } ] - }, - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id-2", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": [ - "https://advertiser.example.com" - ], - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "cat": [ - "IAB9-1" - ], - "ext": { - "ix": {} - } - }, - "type": "video" - } - ] } ] } diff --git a/adapters/ix/ixtest/exemplary/multiple-siteIds.json b/adapters/ix/ixtest/exemplary/multiple-siteIds.json new file mode 100644 index 00000000000..7f4227ac4b2 --- /dev/null +++ b/adapters/ix/ixtest/exemplary/multiple-siteIds.json @@ -0,0 +1,224 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569750" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569751" + } + } + } + ], + "site": { + "page": "https://www.example.com/" + }, + "ext": { + "prebid": { + "channel": { + "name": "web", + "version": "7.0.0" + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569750" + } + } + }, + { + "banner": { + "format": [ + { + "h": 600, + "w": 300 + } + ], + "h": 600, + "w": 300 + }, + "ext": { + "bidder": { + "siteId": "569751" + } + }, + "id": "test-imp-id-3" + } + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + } + }, + "ext": { + "ixdiag": { + "multipleSiteIds": "569749, 569750, 569751", + "pbjsv": "7.0.0" + }, + "prebid": { + "channel": { + "name": "web", + "version": "7.0.0" + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json b/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json index 5e0a311a91b..15dc3ecbb0c 100644 --- a/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json +++ b/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json @@ -13,6 +13,10 @@ { "w": 300, "h": 600 + }, + { + "w": 600, + "h": 800 } ] }, @@ -22,7 +26,10 @@ } } } - ] + ], + "site": { + "page": "https://www.example.com/" + } }, "httpCalls": [ { @@ -38,70 +45,16 @@ { "w": 300, "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "ext": { - "bidder": { - "siteId": "569749" - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [ - { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": [ - "https://advertiser.example.com" - ], - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300, - "ext": { - "ix": {} - } - } - ] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - }, - { - "expectedRequest": { - "uri": "http://host/endpoint", - "body": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "banner": { - "format": [ + }, { "w": 300, "h": 600 + }, + { + "w": 600, + "h": 800 } - ], - "w": 300, - "h": 600 + ] }, "ext": { "bidder": { @@ -109,7 +62,13 @@ } } } - ] + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + "id": "569749" + } + } } }, "mockResponse": { @@ -131,7 +90,7 @@ ], "cid": "958", "crid": "29681110", - "h": 600, + "h": 250, "w": 300, "ext": { "ix": {} @@ -171,31 +130,6 @@ "type": "banner" } ] - }, - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": [ - "https://advertiser.example.com" - ], - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 600, - "ext": { - "ix": {} - } - }, - "type": "banner" - } - ] } ] } diff --git a/adapters/ix/ixtest/exemplary/structured-pod.json b/adapters/ix/ixtest/exemplary/structured-pod.json new file mode 100644 index 00000000000..a5ca9895554 --- /dev/null +++ b/adapters/ix/ixtest/exemplary/structured-pod.json @@ -0,0 +1,236 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560, + "podid": "1", + "slotinpod": 1, + "podseq": 1 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560, + "podid": "1", + "slotinpod": -1, + "podseq": 1 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560, + "podid": "1", + "slotinpod": 1, + "podseq": 1 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560, + "podid": "1", + "slotinpod": -1, + "podseq": 1 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 560, + "w": 940, + "dur": 30, + "ext": { + "ix": {} + } + } + ] + }, + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.75, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 560, + "w": 940, + "dur": 30, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 560, + "w": 940, + "dur": 30, + "ext": { + "ix": {} + } + }, + "type": "video" + }, + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.75, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 560, + "w": 940, + "dur": 30, + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/app-site-id-publisher.json b/adapters/ix/ixtest/supplemental/app-site-id-publisher.json new file mode 100644 index 00000000000..aec2c124ca9 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/app-site-id-publisher.json @@ -0,0 +1,144 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "app": { + "domain": "https://www.example.com/", + "publisher": { + "name": "https://www.example.com/" + } + }, + "ext": { + "prebid": { + "channel": { + "name": "web", + "version": "7.0.0" + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "app": { + "domain": "https://www.example.com/", + "publisher": { + "id": "569749", + "name": "https://www.example.com/" + } + }, + "ext": { + "ixdiag": { + "pbjsv": "7.0.0" + }, + "prebid": { + "channel": { + "name": "web", + "version": "7.0.0" + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/bad-imp-id.json b/adapters/ix/ixtest/supplemental/bad-imp-id.json index 5ca8daf234e..28d40443461 100644 --- a/adapters/ix/ixtest/supplemental/bad-imp-id.json +++ b/adapters/ix/ixtest/supplemental/bad-imp-id.json @@ -82,6 +82,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unmatched impression id: bad-imp-id", diff --git a/adapters/ix/ixtest/supplemental/multi-imp-requests-error.json b/adapters/ix/ixtest/supplemental/multi-imp-requests-error.json new file mode 100644 index 00000000000..223e863284c --- /dev/null +++ b/adapters/ix/ixtest/supplemental/multi-imp-requests-error.json @@ -0,0 +1,189 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749", + "sid": 12345 + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 600 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 600, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 600, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ], + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go struct field ExtImpIx.sid of type string", + "comparison": "literal" + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/sid.json b/adapters/ix/ixtest/supplemental/sid.json new file mode 100644 index 00000000000..3d6862d1235 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/sid.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749", + "sid": "1234" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569750", + "sid": "5678" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "ext": { + "ixdiag": { + "multipleSiteIds": "569749, 569750" + }, + "prebid": null + }, + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749", + "sid": "1234" + }, + "sid": "1234" + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569750", + "sid": "5678" + }, + "sid": "5678" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/ix/params_test.go b/adapters/ix/params_test.go index 9246a43a725..8ba937c12f4 100644 --- a/adapters/ix/params_test.go +++ b/adapters/ix/params_test.go @@ -38,6 +38,7 @@ var validParams = []string{ `{"siteID":"12345"}`, `{"siteId":"123456"}`, `{"siteid":"1234567", "size": [640,480]}`, + `{"siteId":"123456", "sid":"12345"}`, } var invalidParams = []string{ diff --git a/adapters/kargo/params_test.go b/adapters/kargo/params_test.go index 44937637480..cb9ffaf1028 100644 --- a/adapters/kargo/params_test.go +++ b/adapters/kargo/params_test.go @@ -34,14 +34,20 @@ func TestInvalidParams(t *testing.T) { } var validParams = []string{ + `{"placementId": ""}`, + `{"placementId": "11523"}`, `{"adSlotID": ""}`, `{"adSlotID": "11523"}`, } var invalidParams = []string{ + `{"placementId": 42}`, + `{"placementId": }`, + `{"placementID": "32321"}`, `{"adSlotID": 42}`, `{"adSlotID": }`, `{"adSlotId": "32321"}`, `{"id": }`, `{}`, + `{"placementId": "11523", "adSlotID": "12345"}`, // Can't include both } diff --git a/adapters/kidoz/kidoztest/exemplary/simple-banner.json b/adapters/kidoz/kidoztest/exemplary/simple-banner.json index 81b5f1e5227..bd4c058cdb6 100644 --- a/adapters/kidoz/kidoztest/exemplary/simple-banner.json +++ b/adapters/kidoz/kidoztest/exemplary/simple-banner.json @@ -68,5 +68,19 @@ } } ], - "expectedBidResponses": [] -} \ No newline at end of file + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-bid-id-1", + "impid": "test-impression-id-1", + "price": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/kidoz/kidoztest/exemplary/simple-video.json b/adapters/kidoz/kidoztest/exemplary/simple-video.json index 7c012f95527..7ffb5406625 100644 --- a/adapters/kidoz/kidoztest/exemplary/simple-video.json +++ b/adapters/kidoz/kidoztest/exemplary/simple-video.json @@ -66,5 +66,19 @@ } } ], - "expectedBidResponses": [] -} \ No newline at end of file + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-bid-id-1", + "impid": "test-impression-id-1", + "price": 1 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/liftoff/liftoff.go b/adapters/liftoff/liftoff.go new file mode 100644 index 00000000000..b649da6f631 --- /dev/null +++ b/adapters/liftoff/liftoff.go @@ -0,0 +1,136 @@ +package liftoff + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const SupportedCurrency = "USD" + +type adapter struct { + Endpoint string +} + +type liftoffImpressionExt struct { + *adapters.ExtImpBidder + // Ext represents the vungle extension. + Ext openrtb_ext.ImpExtLiftoff `json:"vungle"` +} + +// Builder builds a new instance of the Liftoff adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + return &adapter{Endpoint: config.Endpoint}, nil +} + +// MakeRequests split impressions into bid requests and change them into the form that liftoff can handle. +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errs []error + requestCopy := *request + for _, imp := range request.Imp { + // Check if imp comes with bid floor amount defined in a foreign currency + if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != SupportedCurrency { + // Convert to US dollars + convertedValue, err := requestInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, SupportedCurrency) + if err != nil { + errs = append(errs, fmt.Errorf("failed to convert currency (err)%s", err.Error())) + continue + } + + // Update after conversion. All imp elements inside request.Imp are shallow copies + // therefore, their non-pointer values are not shared memory and are safe to modify. + imp.BidFloorCur = SupportedCurrency + imp.BidFloor = convertedValue + } + + var impExt liftoffImpressionExt + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + errs = append(errs, fmt.Errorf("failed unmarshalling imp ext (err)%s", err.Error())) + continue + } + + // get placement_reference_id & pub_app_store_id + var bidderImpExt openrtb_ext.ImpExtLiftoff + if err := json.Unmarshal(impExt.Bidder, &bidderImpExt); err != nil { + errs = append(errs, fmt.Errorf("failed unmarshalling bidder imp ext (err)%s", err.Error())) + continue + } + + bidderImpExt.BidToken = requestCopy.User.BuyerUID + impExt.Ext = bidderImpExt + if newImpExt, err := json.Marshal(impExt); err == nil { + imp.Ext = newImpExt + } else { + errs = append(errs, errors.New("failed re-marshalling imp ext")) + continue + } + + imp.TagID = bidderImpExt.PlacementRefID + requestCopy.Imp = []openrtb2.Imp{imp} + // must make a shallow copy for pointers. + requestAppCopy := *request.App + requestAppCopy.ID = bidderImpExt.PubAppStoreID + requestCopy.App = &requestAppCopy + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.Endpoint, + Body: requestJSON, + Headers: http.Header{ + "Content-Type": []string{"application/json"}, + "Accept": []string{"application/json"}, + "X-OpenRTB-Version": []string{"2.5"}, + }, + } + + requests = append(requests, requestData) + } + + return requests, errs +} + +// MakeBids collect bid response from liftoff and change them into the form that Prebid Server can handle. +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + var errs []error + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: openrtb_ext.BidTypeVideo, + } + + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + + return bidResponse, errs +} diff --git a/adapters/liftoff/liftoff_test.go b/adapters/liftoff/liftoff_test.go new file mode 100644 index 00000000000..0cb717dcc5a --- /dev/null +++ b/adapters/liftoff/liftoff_test.go @@ -0,0 +1,21 @@ +package liftoff + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + conf := config.Adapter{ + Endpoint: "https://liftoff.io/bit/t", + } + bidder, buildErr := Builder(openrtb_ext.BidderLiftoff, conf, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 667, DataCenter: "2"}) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "liftofftest", bidder) +} diff --git a/adapters/liftoff/liftofftest/exemplary/app_video_instl.json b/adapters/liftoff/liftofftest/exemplary/app_video_instl.json new file mode 100644 index 00000000000..12a656b0be8 --- /dev/null +++ b/adapters/liftoff/liftofftest/exemplary/app_video_instl.json @@ -0,0 +1,152 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "instl": 1, + "ext": { + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + }, + "ext": { + "prebid": { + "debug": true + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "instl": 1, + "ext": { + "prebid": null, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + }, + "ext": { + "prebid": { + "debug": true + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 300, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/liftoff/liftofftest/exemplary/app_video_rewarded.json b/adapters/liftoff/liftofftest/exemplary/app_video_rewarded.json new file mode 100644 index 00000000000..59c78e12838 --- /dev/null +++ b/adapters/liftoff/liftofftest/exemplary/app_video_rewarded.json @@ -0,0 +1,145 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 300, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/liftoff/liftofftest/supplemental/appid_placementid_check.json b/adapters/liftoff/liftofftest/supplemental/appid_placementid_check.json new file mode 100644 index 00000000000..59c78e12838 --- /dev/null +++ b/adapters/liftoff/liftofftest/supplemental/appid_placementid_check.json @@ -0,0 +1,145 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 300, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/liftoff/liftofftest/supplemental/liftoff_ext_check.json b/adapters/liftoff/liftofftest/supplemental/liftoff_ext_check.json new file mode 100644 index 00000000000..de936f028e3 --- /dev/null +++ b/adapters/liftoff/liftofftest/supplemental/liftoff_ext_check.json @@ -0,0 +1,140 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": null, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/liftoff/liftofftest/supplemental/missing_appid_or_placementid.json b/adapters/liftoff/liftofftest/supplemental/missing_appid_or_placementid.json new file mode 100644 index 00000000000..6095d9c5168 --- /dev/null +++ b/adapters/liftoff/liftofftest/supplemental/missing_appid_or_placementid.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "placement_reference_id": "78910" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "78910", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "placement_reference_id": "78910" + }, + "vungle": { + "app_store_id": "", + "bid_token": "123", + "placement_reference_id": "78910" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 0*", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/liftoff/liftofftest/supplemental/response_code_204.json b/adapters/liftoff/liftofftest/supplemental/response_code_204.json new file mode 100644 index 00000000000..4abefffc5c9 --- /dev/null +++ b/adapters/liftoff/liftofftest/supplemental/response_code_204.json @@ -0,0 +1,102 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": null, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/liftoff/liftofftest/supplemental/response_code_400.json b/adapters/liftoff/liftofftest/supplemental/response_code_400.json new file mode 100644 index 00000000000..de5b0db421e --- /dev/null +++ b/adapters/liftoff/liftofftest/supplemental/response_code_400.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": null, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400.*", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/liftoff/liftofftest/supplemental/response_code_non_200.json b/adapters/liftoff/liftofftest/supplemental/response_code_non_200.json new file mode 100644 index 00000000000..17e1730ac2c --- /dev/null +++ b/adapters/liftoff/liftofftest/supplemental/response_code_non_200.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://liftoff.io/bit/t", + "headers": { + "Content-Type": ["application/json"], + "Accept": ["application/json"], + "X-OpenRTB-Version": ["2.5"] + }, + "body": { + "id": "test-request-id", + "app": { + "id": "123", + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "ext": { + "prebid": null, + "bidder": { + "app_store_id": "123", + "placement_reference_id": "123" + }, + "vungle": { + "bid_token": "123", + "app_store_id": "123", + "placement_reference_id": "123" + } + } + } + ], + "user": { + "buyeruid": "123" + } + } + }, + "mockResponse": { + "status": 403, + "body": {} + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403.*", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/liftoff/param_test.go b/adapters/liftoff/param_test.go new file mode 100644 index 00000000000..d7cd5d73c09 --- /dev/null +++ b/adapters/liftoff/param_test.go @@ -0,0 +1,46 @@ +package liftoff + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{"app_store_id": "12345", "placement_reference_id": "12345"}`, +} + +var invalidParams = []string{ + `{}`, + // placement_reference_id + `{"placement_reference_id": "12345"}`, + `{"app_store_id": "12345"}`, + `{"app_store_id": null, "placement_reference_id": null}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderLiftoff, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderLiftoff, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} diff --git a/adapters/limelightDigital/LimelightDigitaltest/supplemental/empty_object_response.json b/adapters/limelightDigital/LimelightDigitaltest/supplemental/empty_object_response.json index 7f62a76f4f6..2703b9a8fae 100644 --- a/adapters/limelightDigital/LimelightDigitaltest/supplemental/empty_object_response.json +++ b/adapters/limelightDigital/LimelightDigitaltest/supplemental/empty_object_response.json @@ -48,5 +48,5 @@ } } ], - "expectedBidResponses": [] + "expectedBidResponses": [{"currency":"USD","bids":[]}] } diff --git a/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_bid_impid.json b/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_bid_impid.json index 157453c1c7c..371983bd8d9 100644 --- a/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_bid_impid.json +++ b/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_bid_impid.json @@ -68,6 +68,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "bid contains unknown imp id: test-unknown-banner-id", diff --git a/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_imp_media_type.json b/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_imp_media_type.json index 6eebe900627..dcc3f355340 100644 --- a/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_imp_media_type.json +++ b/adapters/limelightDigital/LimelightDigitaltest/supplemental/unknown_imp_media_type.json @@ -52,6 +52,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unknown media type of imp: test-banner-id", diff --git a/adapters/lm_kiviads/lmkiviads.go b/adapters/lm_kiviads/lmkiviads.go new file mode 100644 index 00000000000..c121be3b24b --- /dev/null +++ b/adapters/lm_kiviads/lmkiviads.go @@ -0,0 +1,161 @@ +package lmkiviads + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type bidType struct { + Type string `json:"type"` +} + +type bidExt struct { + Prebid bidType `json:"prebid"` +} + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + tmpl, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint URL template: %v", err) + } + + bidder := &adapter{ + endpoint: tmpl, + } + + return bidder, nil +} + +func (a *adapter) buildEndpointFromRequest(imp *openrtb2.Imp) (string, error) { + var impExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize bidder impression extension: %v", err), + } + } + + var kiviExt openrtb_ext.ExtLmKiviads + if err := json.Unmarshal(impExt.Bidder, &kiviExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize LmKiviads extension: %v", err), + } + } + + endpointParams := macros.EndpointTemplateParams{ + Host: kiviExt.Env, + SourceId: kiviExt.Pid, + } + + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errs []error + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestCopy := *request + for _, imp := range request.Imp { + requestCopy.Imp = []openrtb2.Imp{imp} + + endpoint, err := a.buildEndpointFromRequest(&imp) + if err != nil { + errs = append(errs, err) + continue + } + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + request := &adapters.RequestData{ + Method: http.MethodPost, + Body: requestJSON, + Uri: endpoint, + Headers: headers, + } + + requests = append(requests, request) + } + + return requests, errs +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + if bidderRawResponse.StatusCode == http.StatusServiceUnavailable { + return nil, []error{&errortypes.BadInput{ + Message: "Bidder LmKiviads is unavailable. Please contact the bidder support.", + }} + } + + if err := adapters.CheckResponseStatusCodeForErrors(bidderRawResponse); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(bidderRawResponse.Body, &bidResp); err != nil { + return nil, []error{err} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Array SeatBid cannot be empty", + }} + } + + return prepareBidResponse(bidResp.SeatBid) +} + +func prepareBidResponse(seats []openrtb2.SeatBid) (*adapters.BidderResponse, []error) { + errs := []error{} + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seats)) + + for _, seatBid := range seats { + for bidId, bid := range seatBid.Bid { + var bidExt bidExt + if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse Bid[%d].Ext: %s", bidId, err.Error()), + }) + continue + } + + bidType, err := openrtb_ext.ParseBidType(bidExt.Prebid.Type) + if err != nil { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bid[%d].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got '%s'", bidId, bidExt.Prebid.Type), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[bidId], + BidType: bidType, + }) + } + } + + return bidResponse, errs +} diff --git a/adapters/lm_kiviads/lmkiviads_test.go b/adapters/lm_kiviads/lmkiviads_test.go new file mode 100644 index 00000000000..dfc8a5db0c4 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviads_test.go @@ -0,0 +1,27 @@ +package lmkiviads + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderSmartHub, + config.Adapter{ + Endpoint: "http://pbs.kiviads.live/?pid={{.SourceId}}&host={{.Host}}", + }, + config.Server{ + ExternalUrl: "http://hosturl.com", + GvlID: 1, + DataCenter: "2", + }, + ) + + assert.NoError(t, buildErr) + adapterstest.RunJSONBidderTest(t, "lmkiviadstest", bidder) +} diff --git a/adapters/lm_kiviads/lmkiviadstest/exemplary/banner.json b/adapters/lm_kiviads/lmkiviadstest/exemplary/banner.json new file mode 100644 index 00000000000..68bbd815c2b --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/exemplary/banner.json @@ -0,0 +1,279 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + }, + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=533faf0754cd43ceab591077781b909b&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=533faf0754cd43ceab591077781b909b&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/exemplary/native.json b/adapters/lm_kiviads/lmkiviadstest/exemplary/native.json new file mode 100644 index 00000000000..efd8a80f488 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/exemplary/native.json @@ -0,0 +1,162 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=533faf0754cd43ceab591077781b909b&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/exemplary/video.json b/adapters/lm_kiviads/lmkiviadstest/exemplary/video.json new file mode 100644 index 00000000000..1f694d510f1 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/exemplary/video.json @@ -0,0 +1,202 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=533faf0754cd43ceab591077781b909b&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/bad-response.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/bad-response.json new file mode 100644 index 00000000000..7380a97983b --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/bad-response.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=533faf0754cd43ceab591077781b902&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-mediatype.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-mediatype.json new file mode 100644 index 00000000000..40c8e78d47b --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-mediatype.json @@ -0,0 +1,188 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=533faf0754cd43ceab591077781b902&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "some": "value" + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid[1].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got ''", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-seatbid-0-bid.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-seatbid-0-bid.json new file mode 100644 index 00000000000..15e9a1df59a --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-seatbid-0-bid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-seatbid.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-seatbid.json new file mode 100644 index 00000000000..6d6daebc8b7 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/empty-seatbid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "bidid": "359da97d0384d8a14767029c18fd840d", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Array SeatBid cannot be empty", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-ext-bidder-object.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-ext-bidder-object.json new file mode 100644 index 00000000000..87ade656d2d --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-ext-bidder-object.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": [] + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize LmKiviads extension: json: cannot unmarshal array into Go value of type openrtb_ext.ExtLmKiviads", + "comparison": "literal" + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-ext-object.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-ext-object.json new file mode 100644 index 00000000000..aa215eb3e34 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-ext-object.json @@ -0,0 +1,47 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": "" + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize bidder impression extension: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-mediatype.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-mediatype.json new file mode 100644 index 00000000000..bdf41988541 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/invalid-mediatype.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "wrong" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid[1].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got 'wrong'", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/status-204.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/status-204.json new file mode 100644 index 00000000000..c040fb79535 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/status-204.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/status-400.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/status-400.json new file mode 100644 index 00000000000..256f1134ccd --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/status-400.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 400, + "body": "The Key has a different ad format" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/status-503.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/status-503.json new file mode 100644 index 00000000000..e0ca7f65d79 --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/status-503.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bidder LmKiviads is unavailable. Please contact the bidder support.", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/lm_kiviads/lmkiviadstest/supplemental/unexpected-status.json b/adapters/lm_kiviads/lmkiviadstest/supplemental/unexpected-status.json new file mode 100644 index 00000000000..d176e64105e --- /dev/null +++ b/adapters/lm_kiviads/lmkiviadstest/supplemental/unexpected-status.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://pbs.kiviads.live/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=kivi-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "kivi-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 403, + "body": "Access is denied" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/lm_kiviads/params_test.go b/adapters/lm_kiviads/params_test.go new file mode 100644 index 00000000000..f40ad516684 --- /dev/null +++ b/adapters/lm_kiviads/params_test.go @@ -0,0 +1,53 @@ +package lmkiviads + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{"env":"kivi-stage", "pid":"123456"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderLmKiviads, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected lmkiviads params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"some": "param"}`, + `{"env":"kivi-stage"}`, + `{"pid":"1234"}`, + `{"othervalue":"Lorem ipsum"}`, + `{"env":"kivi-stage", pid:""}`, + `{"env":"", pid:"1234"}`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderLmKiviads, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/lunamedia/lunamedia_test.go b/adapters/lunamedia/lunamedia_test.go index ef109d92573..961cd545303 100644 --- a/adapters/lunamedia/lunamedia_test.go +++ b/adapters/lunamedia/lunamedia_test.go @@ -11,7 +11,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderLunaMedia, config.Adapter{ - Endpoint: "http://api.lunamedia.io/xp/get?pubid={{.PublisherID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "http://rtb.lunamedia.live/?pid={{.PublisherID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { t.Fatalf("Builder returned unexpected error %v", buildErr) diff --git a/adapters/lunamedia/lunamediatest/exemplary/banner.json b/adapters/lunamedia/lunamediatest/exemplary/banner.json index 3b5c417f169..fa92565b814 100644 --- a/adapters/lunamedia/lunamediatest/exemplary/banner.json +++ b/adapters/lunamedia/lunamediatest/exemplary/banner.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "uri": "http://rtb.lunamedia.live/?pid=19f1b372c7548ec1fe734d2c9f8dc688", "body":{ "id": "testid", "imp": [{ diff --git a/adapters/lunamedia/lunamediatest/exemplary/video.json b/adapters/lunamedia/lunamediatest/exemplary/video.json index 82217373e2e..834963ff41d 100644 --- a/adapters/lunamedia/lunamediatest/exemplary/video.json +++ b/adapters/lunamedia/lunamediatest/exemplary/video.json @@ -24,7 +24,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "uri": "http://rtb.lunamedia.live/?pid=19f1b372c7548ec1fe734d2c9f8dc688", "body":{ "id": "testid", "imp": [{ diff --git a/adapters/lunamedia/lunamediatest/supplemental/compat.json b/adapters/lunamedia/lunamediatest/supplemental/compat.json index 5b84d3a5a39..7fc713e0021 100644 --- a/adapters/lunamedia/lunamediatest/supplemental/compat.json +++ b/adapters/lunamedia/lunamediatest/supplemental/compat.json @@ -23,7 +23,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "uri": "http://rtb.lunamedia.live/?pid=19f1b372c7548ec1fe734d2c9f8dc688", "body":{ "id": "testid", "imp": [{ diff --git a/adapters/lunamedia/lunamediatest/supplemental/responseCode.json b/adapters/lunamedia/lunamediatest/supplemental/responseCode.json index 739af044b29..075d121aa1f 100644 --- a/adapters/lunamedia/lunamediatest/supplemental/responseCode.json +++ b/adapters/lunamedia/lunamediatest/supplemental/responseCode.json @@ -33,7 +33,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://api.lunamedia.io/xp/get?pubid=yu", + "uri": "http://rtb.lunamedia.live/?pid=yu", "body": { "id": "testid", "imp": [ diff --git a/adapters/lunamedia/lunamediatest/supplemental/responsebid.json b/adapters/lunamedia/lunamediatest/supplemental/responsebid.json index e9d8c3c543d..f8c5b67a626 100644 --- a/adapters/lunamedia/lunamediatest/supplemental/responsebid.json +++ b/adapters/lunamedia/lunamediatest/supplemental/responsebid.json @@ -33,7 +33,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://api.lunamedia.io/xp/get?pubid=yu", + "uri": "http://rtb.lunamedia.live/?pid=yu", "body": { "id": "testid", "imp": [ diff --git a/adapters/lunamedia/lunamediatest/supplemental/site.json b/adapters/lunamedia/lunamediatest/supplemental/site.json index 81d71554f38..b2610cec06b 100644 --- a/adapters/lunamedia/lunamediatest/supplemental/site.json +++ b/adapters/lunamedia/lunamediatest/supplemental/site.json @@ -33,7 +33,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "uri": "http://rtb.lunamedia.live/?pid=19f1b372c7548ec1fe734d2c9f8dc688", "body": { "id": "testid", "imp": [ diff --git a/adapters/mgidX/mgidX.go b/adapters/mgidX/mgidX.go new file mode 100644 index 00000000000..29d2df4617c --- /dev/null +++ b/adapters/mgidX/mgidX.go @@ -0,0 +1,158 @@ +package mgidX + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + MgidXBidderExt reqBodyExtBidder `json:"bidder"` +} + +type reqBodyExtBidder struct { + Type string `json:"type"` + PlacementID string `json:"placementId,omitempty"` + EndpointID string `json:"endpointId,omitempty"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var err error + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + var bidderExt adapters.ExtImpBidder + var mgidXExt openrtb_ext.ImpExtMgidX + + if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil { + return nil, []error{err} + } + if err = json.Unmarshal(bidderExt.Bidder, &mgidXExt); err != nil { + return nil, []error{err} + } + + impExt := reqBodyExt{MgidXBidderExt: reqBodyExtBidder{}} + + if mgidXExt.PlacementID != "" { + impExt.MgidXBidderExt.PlacementID = mgidXExt.PlacementID + impExt.MgidXBidderExt.Type = "publisher" + } else if mgidXExt.EndpointID != "" { + impExt.MgidXBidderExt.EndpointID = mgidXExt.EndpointID + impExt.MgidXBidderExt.Type = "network" + } else { + continue + } + + finalyImpExt, err := json.Marshal(impExt) + if err != nil { + return nil, []error{err} + } + + reqCopy.Imp[0].Ext = finalyImpExt + + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + return nil, []error{err} + } + + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + } + + if len(adapterRequests) == 0 { + return nil, []error{errors.New("found no valid impressions")} + } + + return adapterRequests, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + + impsMappedByID := make(map[string]openrtb2.Imp, len(request.Imp)) + for i, imp := range request.Imp { + impsMappedByID[request.Imp[i].ID] = imp + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getBidMediaType(&seatBid.Bid[i]) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getBidMediaType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { + var extBid openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &extBid) + if err != nil { + return "", fmt.Errorf("unable to deserialize imp %v bid.ext", bid.ImpID) + } + + if extBid.Prebid == nil { + return "", fmt.Errorf("imp %v with unknown media type", bid.ImpID) + } + + return extBid.Prebid.Type, nil +} diff --git a/adapters/mgidX/mgidX_test.go b/adapters/mgidX/mgidX_test.go new file mode 100644 index 00000000000..5a71750788a --- /dev/null +++ b/adapters/mgidX/mgidX_test.go @@ -0,0 +1,20 @@ +package mgidX + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderEmtv, config.Adapter{ + Endpoint: "http://example.com/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "mgidXtest", bidder) +} diff --git a/adapters/mgidX/mgidXtest/exemplary/endpointId.json b/adapters/mgidX/mgidXtest/exemplary/endpointId.json new file mode 100644 index 00000000000..2c3b74bc33e --- /dev/null +++ b/adapters/mgidX/mgidXtest/exemplary/endpointId.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "mgidX" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mgidX/mgidXtest/exemplary/simple-banner.json b/adapters/mgidX/mgidXtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..69c65439a99 --- /dev/null +++ b/adapters/mgidX/mgidXtest/exemplary/simple-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "mgidX" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mgidX/mgidXtest/exemplary/simple-native.json b/adapters/mgidX/mgidXtest/exemplary/simple-native.json new file mode 100644 index 00000000000..74b2e31f993 --- /dev/null +++ b/adapters/mgidX/mgidXtest/exemplary/simple-native.json @@ -0,0 +1,117 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "mgidX" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mgidX/mgidXtest/exemplary/simple-video.json b/adapters/mgidX/mgidXtest/exemplary/simple-video.json new file mode 100644 index 00000000000..29159388712 --- /dev/null +++ b/adapters/mgidX/mgidXtest/exemplary/simple-video.json @@ -0,0 +1,128 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "mgidX" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mgidX/mgidXtest/exemplary/simple-web-banner.json b/adapters/mgidX/mgidXtest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..6460b410211 --- /dev/null +++ b/adapters/mgidX/mgidXtest/exemplary/simple-web-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "mgidX" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mgidX/mgidXtest/supplemental/bad_media_type.json b/adapters/mgidX/mgidXtest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..22d09ce2f9f --- /dev/null +++ b/adapters/mgidX/mgidXtest/supplemental/bad_media_type.json @@ -0,0 +1,82 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": {} + } + ], + "seat": "mgidX" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "imp test-imp-id with unknown media type", + "comparison": "literal" + } + ] +} diff --git a/adapters/mgidX/mgidXtest/supplemental/bad_response.json b/adapters/mgidX/mgidXtest/supplemental/bad_response.json new file mode 100644 index 00000000000..8c1cc3750db --- /dev/null +++ b/adapters/mgidX/mgidXtest/supplemental/bad_response.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/mgidX/mgidXtest/supplemental/no-valid-impressions.json b/adapters/mgidX/mgidXtest/supplemental/no-valid-impressions.json new file mode 100644 index 00000000000..cc1edd685f9 --- /dev/null +++ b/adapters/mgidX/mgidXtest/supplemental/no-valid-impressions.json @@ -0,0 +1,20 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "found no valid impressions", + "comparison": "literal" + } + ] +} diff --git a/adapters/mgidX/mgidXtest/supplemental/status-204.json b/adapters/mgidX/mgidXtest/supplemental/status-204.json new file mode 100644 index 00000000000..3e536c25a60 --- /dev/null +++ b/adapters/mgidX/mgidXtest/supplemental/status-204.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/mgidX/mgidXtest/supplemental/status-not-200.json b/adapters/mgidX/mgidXtest/supplemental/status-not-200.json new file mode 100644 index 00000000000..4bb3c8d5b53 --- /dev/null +++ b/adapters/mgidX/mgidXtest/supplemental/status-not-200.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/mgidX/params_test.go b/adapters/mgidX/params_test.go new file mode 100644 index 00000000000..85779ec6e8c --- /dev/null +++ b/adapters/mgidX/params_test.go @@ -0,0 +1,47 @@ +package mgidX + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderMgidX, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderMgidX, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "test"}`, + `{"placementId": "1"}`, + `{"endpointId": "test"}`, + `{"endpointId": "1"}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"endpointId": 42}`, + `{"placementId": "1", "endpointId": "1"}`, +} diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/missmatch_bid_id.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/missmatch_bid_id.json index 28a1b6c72d4..391462e8b41 100644 --- a/adapters/mobfoxpb/mobfoxpbtest/supplemental/missmatch_bid_id.json +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/missmatch_bid_id.json @@ -100,10 +100,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to find impression \"test-imp-id-not\"", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/mobilefuse/mobilefuse.go b/adapters/mobilefuse/mobilefuse.go index f46fc5913a3..c13dbe8a6ae 100644 --- a/adapters/mobilefuse/mobilefuse.go +++ b/adapters/mobilefuse/mobilefuse.go @@ -27,6 +27,10 @@ type BidExt struct { Mf ExtMf `json:"mf"` } +type ExtSkadn struct { + Skadn json.RawMessage `json:"skadn"` +} + // Builder builds a new instance of the MobileFuse adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { template, err := template.New("endpointTemplate").Parse(config.Endpoint) @@ -79,7 +83,7 @@ func (adapter *MobileFuseAdapter) MakeBids(incomingRequest *openrtb2.BidRequest, for _, seatbid := range incomingBidResponse.SeatBid { for i := range seatbid.Bid { - bidType := adapter.getBidType(seatbid.Bid[i]) + bidType := getBidType(seatbid.Bid[i]) seatbid.Bid[i].Ext = nil outgoingBidResponse.Bids = append(outgoingBidResponse.Bids, &adapters.TypedBid{ @@ -95,22 +99,18 @@ func (adapter *MobileFuseAdapter) MakeBids(incomingRequest *openrtb2.BidRequest, func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (*adapters.RequestData, []error) { var errs []error - mobileFuseExtension, errs := adapter.getFirstMobileFuseExtension(bidRequest) - + mobileFuseExtension, errs := getFirstMobileFuseExtension(bidRequest) if errs != nil { return nil, errs } endpoint, err := adapter.getEndpoint(mobileFuseExtension) - if err != nil { return nil, append(errs, err) } - validImps := adapter.getValidImps(bidRequest, mobileFuseExtension) - - if len(validImps) == 0 { - err := fmt.Errorf("No valid imps") + validImps, err := getValidImps(bidRequest, mobileFuseExtension) + if err != nil { errs = append(errs, err) return nil, errs } @@ -118,7 +118,6 @@ func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) ( mobileFuseBidRequest := *bidRequest mobileFuseBidRequest.Imp = validImps body, err := json.Marshal(mobileFuseBidRequest) - if err != nil { return nil, append(errs, err) } @@ -135,7 +134,7 @@ func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) ( }, errs } -func (adapter *MobileFuseAdapter) getFirstMobileFuseExtension(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpMobileFuse, []error) { +func getFirstMobileFuseExtension(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpMobileFuse, []error) { var mobileFuseImpExtension openrtb_ext.ExtImpMobileFuse var errs []error @@ -162,13 +161,23 @@ func (adapter *MobileFuseAdapter) getFirstMobileFuseExtension(request *openrtb2. return &mobileFuseImpExtension, errs } +func getMobileFuseExtensionForImp(imp *openrtb2.Imp, mobileFuseImpExtension *openrtb_ext.ExtImpMobileFuse) error { + var bidder_imp_extension adapters.ExtImpBidder + + err := json.Unmarshal(imp.Ext, &bidder_imp_extension) + if err != nil { + return err + } + + return json.Unmarshal(bidder_imp_extension.Bidder, &mobileFuseImpExtension) +} + func (adapter *MobileFuseAdapter) getEndpoint(ext *openrtb_ext.ExtImpMobileFuse) (string, error) { publisher_id := strconv.Itoa(ext.PublisherId) - url, errs := macros.ResolveMacros(adapter.EndpointTemplate, macros.EndpointTemplateParams{PublisherID: publisher_id}) - - if errs != nil { - return "", errs + url, err := macros.ResolveMacros(adapter.EndpointTemplate, macros.EndpointTemplateParams{PublisherID: publisher_id}) + if err != nil { + return "", err } if ext.TagidSrc == "ext" { @@ -178,23 +187,45 @@ func (adapter *MobileFuseAdapter) getEndpoint(ext *openrtb_ext.ExtImpMobileFuse) return url, nil } -func (adapter *MobileFuseAdapter) getValidImps(bidRequest *openrtb2.BidRequest, ext *openrtb_ext.ExtImpMobileFuse) []openrtb2.Imp { +func getValidImps(bidRequest *openrtb2.BidRequest, ext *openrtb_ext.ExtImpMobileFuse) ([]openrtb2.Imp, error) { var validImps []openrtb2.Imp for _, imp := range bidRequest.Imp { if imp.Banner != nil || imp.Video != nil || imp.Native != nil { + err := getMobileFuseExtensionForImp(&imp, ext) + if err != nil { + return nil, err + } + imp.TagID = strconv.Itoa(ext.PlacementId) - imp.Ext = nil - validImps = append(validImps, imp) - break + var extSkadn ExtSkadn + err = json.Unmarshal(imp.Ext, &extSkadn) + if err != nil { + return nil, err + } + + if extSkadn.Skadn != nil { + imp.Ext, err = json.Marshal(map[string]json.RawMessage{"skadn": extSkadn.Skadn}) + if err != nil { + return nil, err + } + } else { + imp.Ext = nil + } + + validImps = append(validImps, imp) } } - return validImps + if len(validImps) == 0 { + return nil, fmt.Errorf("No valid imps") + } + + return validImps, nil } -func (adapter *MobileFuseAdapter) getBidType(bid openrtb2.Bid) openrtb_ext.BidType { +func getBidType(bid openrtb2.Bid) openrtb_ext.BidType { if bid.Ext != nil { var bidExt BidExt err := json.Unmarshal(bid.Ext, &bidExt) diff --git a/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps-multi-format.json b/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps-multi-format.json new file mode 100644 index 00000000000..c31cb1ced55 --- /dev/null +++ b/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps-multi-format.json @@ -0,0 +1,154 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 320, + "h": 480 + }, + "ext": { + "bidder": { + "placement_id": 123456, + "pub_id": 1234 + } + } + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "native": { + "ver": "1.2", + "request": "{\"native\":{\"assets\":[{\"id\":0,\"required\":1,\"img\":{\"url\":\"...\"}},{\"id\":1,\"required\":1,\"title\":{\"text\":\"custom title\"}}]}}" + }, + "ext": { + "bidder": { + "placement_id": 234567, + "pub_id": 1234 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://mfx.mobilefuse.com/openrtb?pub_id=1234", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 320, + "h": 480 + }, + "tagid": "123456" + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "native": { + "ver": "1.2", + "request": "{\"native\":{\"assets\":[{\"id\":0,\"required\":1,\"img\":{\"url\":\"...\"}},{\"id\":1,\"required\":1,\"title\":{\"text\":\"custom title\"}}]}}" + }, + "tagid": "234567" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "mobilefuse", + "bid": [ + { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 50, + "w": 320, + "ext": { + "mf": { + "media_type": "banner" + } + } + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "1", + "price": 1.50, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps.json b/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps.json index 568e51af61b..15ba4f96d6b 100644 --- a/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps.json +++ b/adapters/mobilefuse/mobilefusetest/exemplary/multi-imps.json @@ -57,6 +57,18 @@ ] }, "tagid": "123456" + }, + { + "id": "2", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "tagid": "234567" } ] } diff --git a/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json b/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json new file mode 100644 index 00000000000..e8fd66b1111 --- /dev/null +++ b/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "skadn": { + "versions": [ + "2.0", + "2.1" + ], + "sourceapp": "11111", + "skadnetids": [ + "424m5254lk.skadnetwork", + "4fzdc2evr5.skadnetwork" + ] + }, + "bidder": { + "placement_id": 999999, + "pub_id": 1111, + "tagid_src": "ext" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://mfx.mobilefuse.com/openrtb?pub_id=1111&tagid_src=ext", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "tagid": "999999", + "ext": { + "skadn": { + "versions": [ + "2.0", + "2.1" + ], + "sourceapp": "11111", + "skadnetids": [ + "424m5254lk.skadnetwork", + "4fzdc2evr5.skadnetwork" + ] + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/motorik/motorik.go b/adapters/motorik/motorik.go new file mode 100644 index 00000000000..c804c0951fc --- /dev/null +++ b/adapters/motorik/motorik.go @@ -0,0 +1,175 @@ +package motorik + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + return bidder, nil +} + +func getHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("User-Agent", request.Device.UA) + } + + if len(request.Device.IPv6) > 0 { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + } + + return headers +} + +func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { + motorikExt, err := getImpressionExt(&openRTBRequest.Imp[0]) + if err != nil { + return nil, []error{err} + } + + openRTBRequest.Imp[0].Ext = nil + + url, err := a.buildEndpointURL(motorikExt) + if err != nil { + return nil, []error{err} + } + + reqJSON, err := json.Marshal(openRTBRequest) + if err != nil { + return nil, []error{err} + } + + return []*adapters.RequestData{{ + Method: http.MethodPost, + Body: reqJSON, + Uri: url, + Headers: getHeaders(openRTBRequest), + }}, nil +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtMotorik, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing motorikExt - " + err.Error(), + } + } + var motorikExt openrtb_ext.ExtMotorik + if err := json.Unmarshal(bidderExt.Bidder, &motorikExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing bidderExt - " + err.Error(), + } + } + + return &motorikExt, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtMotorik) (string, error) { + endpointParams := macros.EndpointTemplateParams{AccountID: params.AccountID, SourceId: params.PlacementID} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func checkResponseStatusCodes(response *adapters.ResponseData) error { + if response.StatusCode == http.StatusServiceUnavailable { + return &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Something went wrong Status Code: [ %d ] ", response.StatusCode), + } + } + + return adapters.CheckResponseStatusCodeForErrors(response) +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + httpStatusError := checkResponseStatusCodes(bidderRawResponse) + if httpStatusError != nil { + return nil, []error{httpStatusError} + } + + responseBody := bidderRawResponse.Body + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bad Server Response", + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + var bidsArray []*adapters.TypedBid + + for _, sb := range bidResp.SeatBid { + for idx, bid := range sb.Bid { + bidType, err := getMediaTypeForImp(bid.ImpID, openRTBRequest.Imp) + if err != nil { + return nil, []error{err} + } + + bidsArray = append(bidsArray, &adapters.TypedBid{ + Bid: &sb.Bid[idx], + BidType: bidType, + }) + } + } + + bidResponse.Bids = bidsArray + return bidResponse, nil +} + +func getMediaTypeForImp(impId string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { + for _, imp := range imps { + if imp.ID == impId { + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } else if imp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } else if imp.Native != nil { + return openrtb_ext.BidTypeNative, nil + } + break + } + } + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\"", impId), + } +} diff --git a/adapters/motorik/motorik_test.go b/adapters/motorik/motorik_test.go new file mode 100644 index 00000000000..1a14fedac7b --- /dev/null +++ b/adapters/motorik/motorik_test.go @@ -0,0 +1,28 @@ +package motorik + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderMotorik, config.Adapter{ + Endpoint: "http://us.example.com/?k={{.AccountID}}&name={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "motoriktest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderMotorik, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/motorik/motoriktest/exemplary/banner-app.json b/adapters/motorik/motoriktest/exemplary/banner-app.json new file mode 100644 index 00000000000..18d8efc7dbb --- /dev/null +++ b/adapters/motorik/motoriktest/exemplary/banner-app.json @@ -0,0 +1,150 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + } + ], + "type": "banner", + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/motorik/motoriktest/exemplary/banner-web.json b/adapters/motorik/motoriktest/exemplary/banner-web.json new file mode 100644 index 00000000000..024b553a9dd --- /dev/null +++ b/adapters/motorik/motoriktest/exemplary/banner-web.json @@ -0,0 +1,193 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + }, + { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + } + ], + "type": "banner", + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50 + }, + "type": "banner" + }, + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/motorik/motoriktest/exemplary/native-app.json b/adapters/motorik/motoriktest/exemplary/native-app.json new file mode 100644 index 00000000000..2f917f02da1 --- /dev/null +++ b/adapters/motorik/motoriktest/exemplary/native-app.json @@ -0,0 +1,147 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20" + } + ], + "type": "native", + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ] + }, + "type": "native" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/motorik/motoriktest/exemplary/native-web.json b/adapters/motorik/motoriktest/exemplary/native-web.json new file mode 100644 index 00000000000..7ffc0bb24ee --- /dev/null +++ b/adapters/motorik/motoriktest/exemplary/native-web.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "2607:fb90:f27:4512:d800:cb23:a603:e245" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20" + } + ], + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ] + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/motorik/motoriktest/exemplary/video-app.json b/adapters/motorik/motoriktest/exemplary/video-app.json new file mode 100644 index 00000000000..caee18cdf03 --- /dev/null +++ b/adapters/motorik/motoriktest/exemplary/video-app.json @@ -0,0 +1,159 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720 + } + ], + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 1280, + "h": 720 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/motorik/motoriktest/exemplary/video-web.json b/adapters/motorik/motoriktest/exemplary/video-web.json new file mode 100644 index 00000000000..0c3afebce99 --- /dev/null +++ b/adapters/motorik/motoriktest/exemplary/video-web.json @@ -0,0 +1,157 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type":"video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/motorik/motoriktest/supplemental/bad_media_type.json b/adapters/motorik/motoriktest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..103b9cf85af --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/bad_media_type.json @@ -0,0 +1,135 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "test-imp-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + } + ], + "type": "banner", + "seat": "motorik" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Failed to find impression \"test-imp-id\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/motorik/motoriktest/supplemental/empty-seatbid-array.json b/adapters/motorik/motoriktest/supplemental/empty-seatbid-array.json new file mode 100644 index 00000000000..9447d661041 --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/empty-seatbid-array.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + ], + "cur": "USD" + } + } + } + ], + "mockResponse": { + "status": 200, + "body": "invalid response" + }, + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} diff --git a/adapters/motorik/motoriktest/supplemental/invalid-bidder-ext-object.json b/adapters/motorik/motoriktest/supplemental/invalid-bidder-ext-object.json new file mode 100644 index 00000000000..fb89a5894a5 --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/invalid-bidder-ext-object.json @@ -0,0 +1,33 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt - json: cannot unmarshal string into Go value of type openrtb_ext.ExtMotorik", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": "wrongBidderExt" + } + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/motorik/motoriktest/supplemental/invalid-motorik-ext-object.json b/adapters/motorik/motoriktest/supplemental/invalid-motorik-ext-object.json new file mode 100644 index 00000000000..1b2cce8db49 --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/invalid-motorik-ext-object.json @@ -0,0 +1,31 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing motorikExt - json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "wrongMotorikExt" + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/motorik/motoriktest/supplemental/invalid-response.json b/adapters/motorik/motoriktest/supplemental/invalid-response.json new file mode 100644 index 00000000000..c497f9df90a --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/invalid-response.json @@ -0,0 +1,112 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w":320, + "h":50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Bad Server Response", + "comparison": "literal" + } + ] +} diff --git a/adapters/motorik/motoriktest/supplemental/status-code-bad-request.json b/adapters/motorik/motoriktest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..293c74b839a --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/status-code-bad-request.json @@ -0,0 +1,93 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/motorik/motoriktest/supplemental/status-code-no-content.json b/adapters/motorik/motoriktest/supplemental/status-code-no-content.json new file mode 100644 index 00000000000..095b869e3d7 --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/status-code-no-content.json @@ -0,0 +1,69 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [{ + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "imp": [{ + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + }], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 204 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/motorik/motoriktest/supplemental/status-code-other-error.json b/adapters/motorik/motoriktest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..95e607bed04 --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/status-code-other-error.json @@ -0,0 +1,79 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 306 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 306. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/motorik/motoriktest/supplemental/status-code-service-unavailable.json b/adapters/motorik/motoriktest/supplemental/status-code-service-unavailable.json new file mode 100644 index 00000000000..aa03e6f1c27 --- /dev/null +++ b/adapters/motorik/motoriktest/supplemental/status-code-service-unavailable.json @@ -0,0 +1,79 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://us.example.com/?k=accountId&name=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 503 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Something went wrong Status Code: [ 503 ] ", + "comparison": "literal" + } + ] +} diff --git a/adapters/motorik/params_test.go b/adapters/motorik/params_test.go new file mode 100644 index 00000000000..9ee05b58e07 --- /dev/null +++ b/adapters/motorik/params_test.go @@ -0,0 +1,50 @@ +package motorik + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{ "accountId": "hash", "placementId": "hash"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderMotorik, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Motorik params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"adCode": "string", "seatCode": 5, "originalPublisherid": "string"}`, + `{ "accountid": "" }`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderMotorik, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/nobid/nobidtest/supplemental/bad-response.json b/adapters/nobid/nobidtest/supplemental/bad-response.json index 05b6e623338..617f92734d0 100644 --- a/adapters/nobid/nobidtest/supplemental/bad-response.json +++ b/adapters/nobid/nobidtest/supplemental/bad-response.json @@ -60,7 +60,7 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to find impression \"XYZ\"", @@ -68,4 +68,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/openrtb_util.go b/adapters/openrtb_util.go deleted file mode 100644 index f04c48297da..00000000000 --- a/adapters/openrtb_util.go +++ /dev/null @@ -1,36 +0,0 @@ -package adapters - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/prebid/openrtb/v19/openrtb2" - "github.com/prebid/prebid-server/openrtb_ext" -) - -func ExtractReqExtBidderParamsMap(bidRequest *openrtb2.BidRequest) (map[string]json.RawMessage, error) { - if bidRequest == nil { - return nil, errors.New("error bidRequest should not be nil") - } - - reqExt := &openrtb_ext.ExtRequest{} - if len(bidRequest.Ext) > 0 { - err := json.Unmarshal(bidRequest.Ext, &reqExt) - if err != nil { - return nil, fmt.Errorf("error decoding Request.ext : %s", err.Error()) - } - } - - if reqExt.Prebid.BidderParams == nil { - return nil, nil - } - - var bidderParams map[string]json.RawMessage - err := json.Unmarshal(reqExt.Prebid.BidderParams, &bidderParams) - if err != nil { - return nil, err - } - - return bidderParams, nil -} diff --git a/adapters/openrtb_util_test.go b/adapters/openrtb_util_test.go deleted file mode 100644 index bb70392eb4d..00000000000 --- a/adapters/openrtb_util_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package adapters - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/prebid/openrtb/v19/openrtb2" - "github.com/stretchr/testify/assert" -) - -func TestExtractAdapterReqBidderParamsMap(t *testing.T) { - tests := []struct { - name string - givenBidRequest *openrtb2.BidRequest - want map[string]json.RawMessage - wantErr error - }{ - { - name: "nil req", - givenBidRequest: nil, - want: nil, - wantErr: errors.New("error bidRequest should not be nil"), - }, - { - name: "nil req.ext", - givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{}}`)}, - want: nil, - wantErr: nil, - }, - { - name: "malformed req.ext", - givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage("malformed")}, - want: nil, - wantErr: errors.New("error decoding Request.ext : invalid character 'm' looking for beginning of value"), - }, - { - name: "extract bidder params from req.Ext for input request in adapter code", - givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"bidderparams": {"profile": 1234, "version": 1}}}`)}, - want: map[string]json.RawMessage{"profile": json.RawMessage(`1234`), "version": json.RawMessage(`1`)}, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ExtractReqExtBidderParamsMap(tt.givenBidRequest) - assert.Equal(t, tt.wantErr, err, "err") - assert.Equal(t, tt.want, got, "result") - }) - } -} diff --git a/adapters/openweb/openwebtest/supplemental/wrong-impression-mapping.json b/adapters/openweb/openwebtest/supplemental/wrong-impression-mapping.json index 776cbabcb0c..d733df3ac3e 100644 --- a/adapters/openweb/openwebtest/supplemental/wrong-impression-mapping.json +++ b/adapters/openweb/openwebtest/supplemental/wrong-impression-mapping.json @@ -68,6 +68,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", diff --git a/adapters/orbidder/orbidder.go b/adapters/orbidder/orbidder.go index 43c3574b4a7..b4f1a16c114 100644 --- a/adapters/orbidder/orbidder.go +++ b/adapters/orbidder/orbidder.go @@ -125,19 +125,51 @@ func (rcv OrbidderAdapter) MakeBids(_ *openrtb2.BidRequest, _ *adapters.RequestD return nil, []error{err} } + var bidErrs []error bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) for _, seatBid := range bidResp.SeatBid { - for _, bid := range seatBid.Bid { + for i := range seatBid.Bid { + // later we have to add the bid as a pointer, + // because of this we need a variable that only exists at this loop iteration. + // otherwise there will be issues with multibid and pointer behavior. + bid := seatBid.Bid[i] + bidType, err := getBidType(bid) + if err != nil { + // could not determinate media type, append an error and continue with the next bid. + bidErrs = append(bidErrs, err) + continue + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &bid, - BidType: openrtb_ext.BidTypeBanner, + BidType: bidType, }) } } if bidResp.Cur != "" { bidResponse.Currency = bidResp.Cur } - return bidResponse, nil + + return bidResponse, bidErrs +} + +func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + + // determinate media type by bid response field mtype + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Could not define media type for impression: %s", bid.ImpID), + } } // Builder builds a new instance of the Orbidder adapter for the given bidder with the given config. diff --git a/adapters/orbidder/orbiddertest/exemplary/multibid-multi-format-with-mtype.json b/adapters/orbidder/orbiddertest/exemplary/multibid-multi-format-with-mtype.json new file mode 100644 index 00000000000..011acb2c7e2 --- /dev/null +++ b/adapters/orbidder/orbiddertest/exemplary/multibid-multi-format-with-mtype.json @@ -0,0 +1,185 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://test.orbidder.de" + }, + "user": { + "buyeruid": "XX672XXX-5XXd-4XX2-8XX6-6XXXXc9cXXXX.v1" + }, + "imp": [ + { + "id": "multi-format-test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "native": { + "ver":"1.2", + "request":"\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":140},{\"id\":1,\"img\":{\"h\":250,\"hmin\":0,\"type\":3,\"w\":300,\"wmin\":0}}]" + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "web-test-placement-1", + "bidfloor": 0.1 + } + } + },{ + "id": "multi-format-test-imp-id-2", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "native": { + "ver":"1.2", + "request":"\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":140},{\"id\":1,\"img\":{\"h\":250,\"hmin\":0,\"type\":3,\"w\":300,\"wmin\":0}}]" + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "web-test-placement-2", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "site": { + "page": "https://test.orbidder.de" + }, + "user": { + "buyeruid": "XX672XXX-5XXd-4XX2-8XX6-6XXXXc9cXXXX.v1" + }, + "imp": [ + { + "id": "multi-format-test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "native": { + "ver":"1.2", + "request":"\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":140},{\"id\":1,\"img\":{\"h\":250,\"hmin\":0,\"type\":3,\"w\":300,\"wmin\":0}}]" + }, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "web-test-placement-1", + "bidfloor": 0.1 + } + } + },{ + "id": "multi-format-test-imp-id-2", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "native": { + "ver":"1.2", + "request":"\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":140},{\"id\":1,\"img\":{\"h\":250,\"hmin\":0,\"type\":3,\"w\":300,\"wmin\":0}}]" + }, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "web-test-placement-2", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e900", + "impid": "multi-format-test-imp-id-1", + "adid": "11110126", + "price": 0.600000, + "adm": "banner-some-test-ad", + "crid": "banner-test-crid", + "h": 250, + "w": 300, + "mtype": 1 + },{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "multi-format-test-imp-id-2", + "adid": "11110136", + "price": 0.500000, + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"text\":\"Orbidder rocks\"}},{\"id\":1,\"required\":1,\"img\":{\"url\":\"https://prd.orbidder.de/common/logo.png\"}}],\"link\":{\"url\":\"https://www.otto.de/\",\"clicktrackers\":[\"https://prd.orbidder.de/click\"]},\"imptrackers\":[\"https://prd.orbidder.de/imp\"]}", + "crid": "native-test-crid", + "mtype": 4 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e900", + "impid": "multi-format-test-imp-id-1", + "adid": "11110126", + "price": 0.6, + "adm": "banner-some-test-ad", + "crid": "banner-test-crid", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + },{ + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "multi-format-test-imp-id-2", + "adid": "11110136", + "price": 0.5, + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"text\":\"Orbidder rocks\"}},{\"id\":1,\"required\":1,\"img\":{\"url\":\"https://prd.orbidder.de/common/logo.png\"}}],\"link\":{\"url\":\"https://www.otto.de/\",\"clicktrackers\":[\"https://prd.orbidder.de/click\"]},\"imptrackers\":[\"https://prd.orbidder.de/imp\"]}", + "crid": "native-test-crid", + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json b/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json index 2315a52c130..80b3c794c5a 100644 --- a/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json +++ b/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json @@ -79,7 +79,8 @@ "adm": "some-test-ad", "crid": "test-crid", "h": 250, - "w": 300 + "w": 300, + "mtype": 1 } ] } @@ -102,7 +103,8 @@ "adm": "some-test-ad", "crid": "test-crid", "w": 300, - "h": 250 + "h": 250, + "mtype": 1 }, "type": "banner" } diff --git a/adapters/orbidder/orbiddertest/exemplary/simple-web-banner.json b/adapters/orbidder/orbiddertest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..9e1181a590b --- /dev/null +++ b/adapters/orbidder/orbiddertest/exemplary/simple-web-banner.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://test.orbidder.de" + }, + "user": { + "buyeruid": "XX672XXX-5XXd-4XX2-8XX6-6XXXXc9cXXXX.v1" + }, + "imp": [ + { + "id": "web-test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "web-test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "site": { + "page": "https://test.orbidder.de" + }, + "user": { + "buyeruid": "XX672XXX-5XXd-4XX2-8XX6-6XXXXc9cXXXX.v1" + }, + "imp": [ + { + "id": "web-test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "web-test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-test-request-id", + "seatbid": [ + { + "seat": "web-seat-id", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "web-test-imp-id", + "adid": "11110126", + "price": 0.600000, + "adm": "web-some-test-ad", + "crid": "web-test-crid", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "web-test-imp-id", + "adid": "11110126", + "price": 0.6, + "adm": "web-some-test-ad", + "crid": "web-test-crid", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/orbidder/orbiddertest/exemplary/simple-web-native.json b/adapters/orbidder/orbiddertest/exemplary/simple-web-native.json new file mode 100644 index 00000000000..f4b8ec2c7db --- /dev/null +++ b/adapters/orbidder/orbiddertest/exemplary/simple-web-native.json @@ -0,0 +1,102 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "test.orbidder.de" + }, + "user": { + "buyeruid": "test-user" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver":"1.2", + "request":"\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":140},{\"id\":1,\"img\":{\"h\":250,\"hmin\":0,\"type\":3,\"w\":300,\"wmin\":0}}]" + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "site": { + "page": "test.orbidder.de" + }, + "user": { + "buyeruid": "test-user" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver":"1.2", + "request":"\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":140},{\"id\":1,\"img\":{\"h\":250,\"hmin\":0,\"type\":3,\"w\":300,\"wmin\":0}}]" + }, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"text\":\"Orbidder rocks\"}},{\"id\":1,\"required\":1,\"img\":{\"url\":\"https://prd.orbidder.de/common/logo.png\"}}],\"link\":{\"url\":\"https://www.otto.de/\",\"clicktrackers\":[\"https://prd.orbidder.de/click\"]},\"imptrackers\":[\"https://prd.orbidder.de/imp\"]}", + "crid": "test-crid", + "mtype": 4 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.5, + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"text\":\"Orbidder rocks\"}},{\"id\":1,\"required\":1,\"img\":{\"url\":\"https://prd.orbidder.de/common/logo.png\"}}],\"link\":{\"url\":\"https://www.otto.de/\",\"clicktrackers\":[\"https://prd.orbidder.de/click\"]},\"imptrackers\":[\"https://prd.orbidder.de/imp\"]}", + "crid": "test-crid", + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json b/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json index 1ea133ab1d3..5b4a523f261 100644 --- a/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json +++ b/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json @@ -85,7 +85,8 @@ "adm": "some-test-ad", "crid": "test-crid", "h": 250, - "w": 300 + "w": 300, + "mtype": 1 } ] } @@ -108,7 +109,8 @@ "adm": "some-test-ad", "crid": "test-crid", "w": 300, - "h": 250 + "h": 250, + "mtype": 1 }, "type": "banner" } diff --git a/adapters/ownadx/ownadx.go b/adapters/ownadx/ownadx.go new file mode 100644 index 00000000000..c19343ba47f --- /dev/null +++ b/adapters/ownadx/ownadx.go @@ -0,0 +1,213 @@ +package ownadx + +import ( + "encoding/json" + "fmt" + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" + "text/template" +) + +type adapter struct { + endpoint *template.Template +} +type bidExt struct { + MediaType string `json:"mediaType"` +} + +func (adapter *adapter) getRequestData(bidRequest *openrtb2.BidRequest, impExt *openrtb_ext.ExtImpOwnAdx, imps []openrtb2.Imp) (*adapters.RequestData, error) { + pbidRequest := createBidRequest(bidRequest, imps) + reqJSON, err := json.Marshal(pbidRequest) + if err != nil { + return nil, &errortypes.BadInput{ + Message: "Prebid bidder request not valid or can't be marshalled. Err: " + err.Error(), + } + } + url, err := adapter.buildEndpointURL(impExt) + if err != nil { + return nil, &errortypes.BadInput{ + Message: "Error while creating endpoint. Err: " + err.Error(), + } + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("x-openrtb-version", "2.5") + + return &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: reqJSON, + Headers: headers}, nil + +} +func createBidRequest(rtbBidRequest *openrtb2.BidRequest, imps []openrtb2.Imp) *openrtb2.BidRequest { + bidRequest := *rtbBidRequest + bidRequest.Imp = imps + return &bidRequest +} +func (adapter *adapter) buildEndpointURL(params *openrtb_ext.ExtImpOwnAdx) (string, error) { + endpointParams := macros.EndpointTemplateParams{ + ZoneID: params.SspId, + AccountID: params.SeatId, + SourceId: params.TokenId, + } + return macros.ResolveMacros(adapter.endpoint, endpointParams) +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpOwnAdx, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Bidder extension not valid or can't be unmarshalled", + } + } + + var ownAdxExt openrtb_ext.ExtImpOwnAdx + if err := json.Unmarshal(bidderExt.Bidder, &ownAdxExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error while unmarshaling bidder extension", + } + } + + return &ownAdxExt, nil +} + +func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + errs := make([]error, 0, len(request.Imp)) + if len(request.Imp) == 0 { + errs = append(errs, &errortypes.BadInput{ + Message: "No impression in the bid request"}, + ) + return nil, errs + } + extImps, errors := groupImpsByExt(request.Imp) + if len(errors) != 0 { + errs = append(errs, errors...) + } + if len(extImps) == 0 { + return nil, errs + } + reqDetail := make([]*adapters.RequestData, 0, len(extImps)) + for k, imps := range extImps { + bidRequest, err := adapter.getRequestData(request, &k, imps) + if err != nil { + errs = append(errs, err) + } else { + reqDetail = append(reqDetail, bidRequest) + } + } + return reqDetail, errs +} +func groupImpsByExt(imps []openrtb2.Imp) (map[openrtb_ext.ExtImpOwnAdx][]openrtb2.Imp, []error) { + respExt := make(map[openrtb_ext.ExtImpOwnAdx][]openrtb2.Imp) + errors := make([]error, 0, len(imps)) + for _, imp := range imps { + ownAdxExt, err := getImpressionExt(&(imp)) + if err != nil { + errors = append(errors, err) + continue + } + + respExt[*ownAdxExt] = append(respExt[*ownAdxExt], imp) + } + return respExt, errors +} + +func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + if response.StatusCode == http.StatusBadRequest { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bad request: %d", response.StatusCode), + }, + } + } + if response.StatusCode != http.StatusOK { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.test = 1 for more info.", response.StatusCode), + }, + } + } + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bad server response "), + }, + } + } + if len(bidResp.SeatBid) == 0 { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Array SeatBid cannot be empty "), + }, + } + } + + seatBid := bidResp.SeatBid[0] + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + if len(seatBid.Bid) == 0 { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bid cannot be empty "), + }, + } + } + for i := 0; i < len(seatBid.Bid); i++ { + var bidType openrtb_ext.BidType + bid := seatBid.Bid[i] + + bidType, err := getMediaType(bid) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bid type is invalid", + }} + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) + } + + return bidResponse, nil +} + +// Builder builds a new instance of the OwnAdx adapter for the given bidder with the given config +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + + return bidder, nil +} + +func getMediaType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("invalid BidType: %d", bid.MType) + } +} diff --git a/adapters/ownadx/ownadx_test.go b/adapters/ownadx/ownadx_test.go new file mode 100644 index 00000000000..9bd7ba70353 --- /dev/null +++ b/adapters/ownadx/ownadx_test.go @@ -0,0 +1,17 @@ +package ownadx + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderOwnAdx, config.Adapter{ + Endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.AccountID}}/{{.ZoneID}}?token={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.NoError(t, buildErr) + adapterstest.RunJSONBidderTest(t, "ownadxtest", bidder) +} diff --git a/adapters/ownadx/ownadxtest/exemplary/banner.json b/adapters/ownadx/ownadxtest/exemplary/banner.json new file mode 100644 index 00000000000..f77321c53b1 --- /dev/null +++ b/adapters/ownadx/ownadxtest/exemplary/banner.json @@ -0,0 +1,166 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "mtype": 1, + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "mediaType": "banner" + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "mtype": 1, + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "mediaType": "banner" + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/exemplary/video.json b/adapters/ownadx/ownadxtest/exemplary/video.json new file mode 100644 index 00000000000..7cac425c7e3 --- /dev/null +++ b/adapters/ownadx/ownadxtest/exemplary/video.json @@ -0,0 +1,196 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 2 + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/bad-server-response.json b/adapters/ownadx/ownadxtest/supplemental/bad-server-response.json new file mode 100644 index 00000000000..61b6da04fa8 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/bad-server-response.json @@ -0,0 +1,108 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200 + + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bad server response ", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/bid-empty-.json b/adapters/ownadx/ownadxtest/supplemental/bid-empty-.json new file mode 100644 index 00000000000..8d40e878925 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/bid-empty-.json @@ -0,0 +1,118 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "seat": "seat" + } + ], + "cur": "USD" + } + + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid cannot be empty ", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/bidext-type.json b/adapters/ownadx/ownadxtest/supplemental/bidext-type.json new file mode 100644 index 00000000000..b1d634287d7 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/bidext-type.json @@ -0,0 +1,141 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid type is invalid", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/http-status-204.json b/adapters/ownadx/ownadxtest/supplemental/http-status-204.json new file mode 100644 index 00000000000..4b7f8663f02 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/http-status-204.json @@ -0,0 +1,51 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/http-status-400.json b/adapters/ownadx/ownadxtest/supplemental/http-status-400.json new file mode 100644 index 00000000000..035bc323e38 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/http-status-400.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bad request: 400", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/invalid-req-empty-imp.json b/adapters/ownadx/ownadxtest/supplemental/invalid-req-empty-imp.json new file mode 100644 index 00000000000..0407ed6cfbf --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/invalid-req-empty-imp.json @@ -0,0 +1,34 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "id", + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [] +} diff --git a/adapters/ownadx/ownadxtest/supplemental/invalid-req-imp-ext.json b/adapters/ownadx/ownadxtest/supplemental/invalid-req-imp-ext.json new file mode 100644 index 00000000000..9b8ba0d1820 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/invalid-req-imp-ext.json @@ -0,0 +1,47 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Bidder extension not valid or can't be unmarshalled", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + } + + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [] +} diff --git a/adapters/ownadx/ownadxtest/supplemental/invalid-req-imp.ext.bidder.json b/adapters/ownadx/ownadxtest/supplemental/invalid-req-imp.ext.bidder.json new file mode 100644 index 00000000000..b04e037d0a2 --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/invalid-req-imp.ext.bidder.json @@ -0,0 +1,49 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error while unmarshaling bidder extension", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": [] + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [] +} diff --git a/adapters/ownadx/ownadxtest/supplemental/seatbid-empty-.json b/adapters/ownadx/ownadxtest/supplemental/seatbid-empty-.json new file mode 100644 index 00000000000..f49ece3ea2f --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/seatbid-empty-.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "cur": "USD" + } + + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Array SeatBid cannot be empty ", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/ownadx/ownadxtest/supplemental/unexpected-status.json b/adapters/ownadx/ownadxtest/supplemental/unexpected-status.json new file mode 100644 index 00000000000..f501a11064e --- /dev/null +++ b/adapters/ownadx/ownadxtest/supplemental/unexpected-status.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://pbs.prebid-ownadx.com/bidder/bid/2/11?token=126151698247", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tokenId": "126151698247", + "sspId": "11", + "seatId": "2" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 403 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403. Run with request.test = 1 for more info.", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/pangle/pangletest/supplemental/pangle_ext_check.json b/adapters/pangle/pangletest/supplemental/pangle_ext_check.json index e80c199a180..4983acfd25d 100644 --- a/adapters/pangle/pangletest/supplemental/pangle_ext_check.json +++ b/adapters/pangle/pangletest/supplemental/pangle_ext_check.json @@ -98,11 +98,11 @@ } } ], - "expectedBidResponses": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "missing pangleExt/adtype in bid ext", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/pangle/pangletest/supplemental/unrecognized_adtype.json b/adapters/pangle/pangletest/supplemental/unrecognized_adtype.json index 613694a98b1..748716862e3 100644 --- a/adapters/pangle/pangletest/supplemental/unrecognized_adtype.json +++ b/adapters/pangle/pangletest/supplemental/unrecognized_adtype.json @@ -100,12 +100,11 @@ } } ], - "expectedBidResponses": [ - ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "unrecognized adtype in response", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go index 4b4fbff7471..04bd00cb179 100644 --- a/adapters/pubmatic/pubmatic.go +++ b/adapters/pubmatic/pubmatic.go @@ -4,12 +4,12 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/http" "strconv" "strings" "github.com/buger/jsonparser" - "github.com/golang/glog" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" @@ -283,8 +283,8 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP if pubmaticExt.Kadfloor != "" { bidfloor, err := strconv.ParseFloat(strings.TrimSpace(pubmaticExt.Kadfloor), 64) if err == nil { - //do not overwrite existing value if kadfloor is invalid - imp.BidFloor = bidfloor + // In case of valid kadfloor, select maximum of original imp.bidfloor and kadfloor + imp.BidFloor = math.Max(bidfloor, imp.BidFloor) } } @@ -389,7 +389,6 @@ func getAlternateBidderCodesFromRequestExt(reqExt *openrtb_ext.ExtRequest) []str func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[string]interface{}) { for _, keyVal := range keywords { if len(keyVal.Values) == 0 { - logf("No values present for key = %s", keyVal.Key) continue } else { key := keyVal.Key @@ -613,12 +612,6 @@ func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType { return bidType } -func logf(msg string, args ...interface{}) { - if glog.V(2) { - glog.Infof(msg, args...) - } -} - // Builder builds a new instance of the Pubmatic adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &PubmaticAdapter{ diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go index 57914228d8e..7553519f990 100644 --- a/adapters/pubmatic/pubmatic_test.go +++ b/adapters/pubmatic/pubmatic_test.go @@ -122,7 +122,7 @@ func TestParseImpressionObject(t *testing.T) { expectedBidfloor: 0.12, }, { - name: "imp.bidfloor set and kadfloor set, preference to kadfloor", + name: "imp.bidfloor set and kadfloor set, higher imp.bidfloor", args: args{ imp: &openrtb2.Imp{ BidFloor: 0.12, @@ -130,7 +130,18 @@ func TestParseImpressionObject(t *testing.T) { Ext: json.RawMessage(`{"bidder":{"kadfloor":"0.11"}}`), }, }, - expectedBidfloor: 0.11, + expectedBidfloor: 0.12, + }, + { + name: "imp.bidfloor set and kadfloor set, higher kadfloor", + args: args{ + imp: &openrtb2.Imp{ + BidFloor: 0.12, + Video: &openrtb2.Video{}, + Ext: json.RawMessage(`{"bidder":{"kadfloor":"0.13"}}`), + }, + }, + expectedBidfloor: 0.13, }, { name: "kadfloor string set with whitespace", diff --git a/adapters/pwbid/pwbid.go b/adapters/pwbid/pwbid.go index 4fac0b0df99..21b564ff124 100644 --- a/adapters/pwbid/pwbid.go +++ b/adapters/pwbid/pwbid.go @@ -9,7 +9,6 @@ import ( "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" - "github.com/prebid/prebid-server/util/httputil" ) type adapter struct { @@ -42,11 +41,11 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { var errors []error - if httputil.IsResponseStatusCodeNoContent(responseData) { + if adapters.IsResponseStatusCodeNoContent(responseData) { return nil, nil } - if err := httputil.CheckResponseStatusCodeForErrors(responseData); err != nil { + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { return nil, []error{err} } diff --git a/adapters/pwbid/pwbid_test.go b/adapters/pwbid/pwbid_test.go index 21a4194249e..194e4bdea02 100644 --- a/adapters/pwbid/pwbid_test.go +++ b/adapters/pwbid/pwbid_test.go @@ -10,7 +10,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderPWBid, config.Adapter{ - Endpoint: "https://bid.pubwise.io/prebid"}, + Endpoint: "https://bidder.east2.pubwise.io/bid/pubwisedirect"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 842, DataCenter: "2"}) if buildErr != nil { diff --git a/adapters/pwbid/pwbidtest/exemplary/banner.json b/adapters/pwbid/pwbidtest/exemplary/banner.json index ba618cb8cf1..4cf93e1ab76 100644 --- a/adapters/pwbid/pwbidtest/exemplary/banner.json +++ b/adapters/pwbid/pwbidtest/exemplary/banner.json @@ -23,7 +23,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id-banner", "imp": [ @@ -90,4 +90,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/adapters/pwbid/pwbidtest/exemplary/native.json b/adapters/pwbid/pwbidtest/exemplary/native.json index 907c16d467a..ff57752c5ea 100644 --- a/adapters/pwbid/pwbidtest/exemplary/native.json +++ b/adapters/pwbid/pwbidtest/exemplary/native.json @@ -18,7 +18,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id-native", "imp": [ @@ -78,4 +78,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/adapters/pwbid/pwbidtest/exemplary/optional-params.json b/adapters/pwbid/pwbidtest/exemplary/optional-params.json index a080be90208..5ababb24bdc 100644 --- a/adapters/pwbid/pwbidtest/exemplary/optional-params.json +++ b/adapters/pwbid/pwbidtest/exemplary/optional-params.json @@ -25,7 +25,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id-banner", "imp": [ @@ -94,4 +94,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/adapters/pwbid/pwbidtest/exemplary/video.json b/adapters/pwbid/pwbidtest/exemplary/video.json index b74c780d0a9..6257de632d4 100644 --- a/adapters/pwbid/pwbidtest/exemplary/video.json +++ b/adapters/pwbid/pwbidtest/exemplary/video.json @@ -20,7 +20,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json index 146ba93a27d..0d469893e0c 100644 --- a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json +++ b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/pwbid/pwbidtest/supplemental/response-204.json b/adapters/pwbid/pwbidtest/supplemental/response-204.json index 5fff7ee32cc..4fc8961e0bb 100644 --- a/adapters/pwbid/pwbidtest/supplemental/response-204.json +++ b/adapters/pwbid/pwbidtest/supplemental/response-204.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/pwbid/pwbidtest/supplemental/response-400.json b/adapters/pwbid/pwbidtest/supplemental/response-400.json index d594e571243..a1517883243 100644 --- a/adapters/pwbid/pwbidtest/supplemental/response-400.json +++ b/adapters/pwbid/pwbidtest/supplemental/response-400.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/pwbid/pwbidtest/supplemental/response-500.json b/adapters/pwbid/pwbidtest/supplemental/response-500.json index fa3d4d063a8..c2b5649418b 100644 --- a/adapters/pwbid/pwbidtest/supplemental/response-500.json +++ b/adapters/pwbid/pwbidtest/supplemental/response-500.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://bid.pubwise.io/prebid", + "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect", "body": { "id": "test-request-id", "imp": [ diff --git a/util/httputil/responseutil.go b/adapters/response.go similarity index 70% rename from util/httputil/responseutil.go rename to adapters/response.go index 14d085f6924..080f46273cc 100644 --- a/util/httputil/responseutil.go +++ b/adapters/response.go @@ -1,14 +1,13 @@ -package httputil +package adapters import ( "fmt" "net/http" - "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" ) -func CheckResponseStatusCodeForErrors(response *adapters.ResponseData) error { +func CheckResponseStatusCodeForErrors(response *ResponseData) error { if response.StatusCode == http.StatusBadRequest { return &errortypes.BadInput{ Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), @@ -22,6 +21,6 @@ func CheckResponseStatusCodeForErrors(response *adapters.ResponseData) error { return nil } -func IsResponseStatusCodeNoContent(response *adapters.ResponseData) bool { +func IsResponseStatusCodeNoContent(response *ResponseData) bool { return response.StatusCode == http.StatusNoContent } diff --git a/util/httputil/responseutil_test.go b/adapters/response_test.go similarity index 61% rename from util/httputil/responseutil_test.go rename to adapters/response_test.go index 5adb0054d56..824a295e73d 100644 --- a/util/httputil/responseutil_test.go +++ b/adapters/response_test.go @@ -1,28 +1,27 @@ -package httputil +package adapters import ( "testing" - "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" "github.com/stretchr/testify/assert" ) func TestCheckResponseStatusCodeForErrors(t *testing.T) { t.Run("bad_input", func(t *testing.T) { - err := CheckResponseStatusCodeForErrors(&adapters.ResponseData{StatusCode: 400}) + err := CheckResponseStatusCodeForErrors(&ResponseData{StatusCode: 400}) expectedErr := &errortypes.BadInput{Message: "Unexpected status code: 400. Run with request.debug = 1 for more info"} assert.Equal(t, expectedErr.Error(), err.Error()) }) t.Run("internal_server_error", func(t *testing.T) { - err := CheckResponseStatusCodeForErrors(&adapters.ResponseData{StatusCode: 500}) + err := CheckResponseStatusCodeForErrors(&ResponseData{StatusCode: 500}) expectedErrMessage := "Unexpected status code: 500. Run with request.debug = 1 for more info" assert.Equal(t, expectedErrMessage, err.Error()) }) } func TestIsResponseStatusCodeNoContent(t *testing.T) { - assert.True(t, IsResponseStatusCodeNoContent(&adapters.ResponseData{StatusCode: 204})) - assert.False(t, IsResponseStatusCodeNoContent(&adapters.ResponseData{StatusCode: 200})) + assert.True(t, IsResponseStatusCodeNoContent(&ResponseData{StatusCode: 204})) + assert.False(t, IsResponseStatusCodeNoContent(&ResponseData{StatusCode: 200})) } diff --git a/adapters/richaudience/richaudience.go b/adapters/richaudience/richaudience.go index 71356bf20f4..16fbe229acf 100644 --- a/adapters/richaudience/richaudience.go +++ b/adapters/richaudience/richaudience.go @@ -52,10 +52,16 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte } if request.App != nil { + appCopy := *request.App + request.App = &appCopy + request.App.Keywords = "tagid=" + imp.TagID } if request.Site != nil { + siteCopy := *request.Site + request.Site = &siteCopy + request.Site.Keywords = "tagid=" + imp.TagID } @@ -65,6 +71,13 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte } if raiExt.Test { + if request.Device != nil { + deviceCopy := *request.Device + request.Device = &deviceCopy + } else { + request.Device = &openrtb2.Device{} + } + request.Device.IP = "11.222.33.44" request.Test = int8(1) } diff --git a/adapters/richaudience/richaudience_test.go b/adapters/richaudience/richaudience_test.go index c9611fbc145..30d04775c44 100644 --- a/adapters/richaudience/richaudience_test.go +++ b/adapters/richaudience/richaudience_test.go @@ -64,7 +64,7 @@ func TestGetBuilder(t *testing.T) { t.Errorf("error %s", buildErr) } - adapterstest.RunJSONBidderTest(t, "richaudience", bidder) + adapterstest.RunJSONBidderTest(t, "richaudiencetest", bidder) } func TestGetSite(t *testing.T) { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json index 4b760494e6b..672d8ab0045 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json @@ -13,6 +13,7 @@ }, "bidfloor": 1e-05, "bidfloorcur": "USD", + "tagid": "testTag", "ext": { "bidder": { "pid": "OsNsyeF68q", @@ -24,7 +25,6 @@ ], "app": { "id": "12345678", - "keywords": "tagid=", "name": "Richaudience TV", "bundle": "R12345678901011", "publisher": { @@ -41,7 +41,7 @@ } }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" }, "user": { @@ -86,7 +86,7 @@ ], "app": { "id": "12345678", - "keywords": "tagid=", + "keywords": "tagid=testTag", "name": "Richaudience TV", "bundle": "R12345678901011", "publisher": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json index 7583c13f38f..0ddec0a4af5 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json @@ -14,6 +14,7 @@ ] }, "bidfloor": 1e-05, + "tagid": "testTag", "ext": { "bidder": { "pid": "OsNsyeF68q", @@ -24,12 +25,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { @@ -80,7 +80,7 @@ } ], "site": { - "keywords": "tagid=", + "keywords": "tagid=testTag", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json index e4bc332740a..5ccc8f141af 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json @@ -13,6 +13,7 @@ }, "bidfloor": 1e-05, "bidfloorcur": "USD", + "tagid": "testTag", "ext": { "bidder": { "pid": "OsNsyeF68q", @@ -23,14 +24,13 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { "dnt": 1, "lmt": 1, - "ip": "11.222.33.44" + "ip": "10.20.30.40" }, "user": { "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a", @@ -73,7 +73,7 @@ } ], "site": { - "keywords": "tagid=", + "keywords": "tagid=testTag", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json index d22e1cf663c..b6227d37873 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json @@ -25,12 +25,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json index 7d76a12b5ed..9fec4d46b17 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json @@ -24,12 +24,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json index 93fabba3d60..b40e9f65c89 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json @@ -25,12 +25,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json new file mode 100644 index 00000000000..f5e3899130a --- /dev/null +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json @@ -0,0 +1,168 @@ +{ + "mockBidRequest": { + "id": "4d3f84eb-787b-42fb-a0cf-062690dadce3", + "test": 0, + "imp": [ + { + "id": "div-gpt-ad-1460505748561-0", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "bidfloor": 1e-05, + "bidfloorcur": "USD", + "tagid": "testTag", + "ext": { + "bidder": { + "pid": "OsNsyeF68q", + "supplyType": "site", + "test": true + } + } + } + ], + "app": { + "id": "12345678", + "name": "Richaudience TV", + "bundle": "R12345678901011", + "publisher": { + "id": "1234567", + "ext": { + "prebid": { + "parentAccount": "891011" + } + } + }, + "content": { + "title": "Richaudience TV", + "series": "Richaudience TV" + } + }, + "user": { + "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a", + "ext": {} + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ortb.richaudience.com/ortb/?bidder=pbs", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + "id": "4d3f84eb-787b-42fb-a0cf-062690dadce3", + "test": 1, + "imp": [ + { + "id": "div-gpt-ad-1460505748561-0", + "tagid": "OsNsyeF68q", + "secure": 0, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "bidfloor": 1e-05, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "pid": "OsNsyeF68q", + "supplyType": "site", + "test": true + } + } + } + ], + "app": { + "id": "12345678", + "keywords": "tagid=testTag", + "name": "Richaudience TV", + "bundle": "R12345678901011", + "publisher": { + "id": "1234567", + "ext": { + "prebid": { + "parentAccount": "891011" + } + } + }, + "content": { + "title": "Richaudience TV", + "series": "Richaudience TV" + } + }, + "device": { + "ip": "11.222.33.44" + }, + "user": { + "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a", + "ext": { + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "ff935bea-4661-40bf-95b7-80c354cf0cdc", + "seatbid": [ + { + "bid": [ + { + "id": "47286888", + "impid": "div-gpt-ad-1460505748561-0", + "price": 99, + "crid": "999999", + "adm": "", + "adomain": [ + "richaudience.com" + ], + "h": 250, + "w": 300 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "47286888", + "impid": "div-gpt-ad-1460505748561-0", + "price": 99, + "adm": "", + "adomain": [ + "richaudience.com" + ], + "crid": "999999", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json index 68cc0af6472..78886f38189 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json @@ -23,12 +23,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "http://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json index 70662052cc3..5490197dd0f 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json @@ -23,12 +23,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json index 901af4a889e..e6a00cbf0e8 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json @@ -23,12 +23,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner.json index 901af4a889e..e6a00cbf0e8 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-banner.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner.json @@ -23,12 +23,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-video.json b/adapters/richaudience/richaudiencetest/exemplary/single-video.json index 2d18c01ae58..0219906f35f 100644 --- a/adapters/richaudience/richaudiencetest/exemplary/single-video.json +++ b/adapters/richaudience/richaudiencetest/exemplary/single-video.json @@ -23,12 +23,11 @@ } ], "site": { - "keywords": "tagid=", "domain": "bridge.richmediastudio.com", "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true" }, "device": { - "ip": "11.222.33.44", + "ip": "10.20.30.40", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" }, "user": { diff --git a/adapters/rise/rise.go b/adapters/rise/rise.go new file mode 100644 index 00000000000..e18b7c93852 --- /dev/null +++ b/adapters/rise/rise.go @@ -0,0 +1,124 @@ +package rise + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/prebid/openrtb/v19/openrtb2" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// adapter is a Rise implementation of the adapters.Bidder interface. +type adapter struct { + endpointURL string +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + return &adapter{ + endpointURL: config.Endpoint, + }, nil +} + +// MakeRequests prepares the HTTP requests which should be made to fetch bids. +func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, _ *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { + org, err := extractOrg(openRTBRequest) + if err != nil { + errs = append(errs, fmt.Errorf("extractOrg: %w", err)) + return nil, errs + } + + openRTBRequestJSON, err := json.Marshal(openRTBRequest) + if err != nil { + errs = append(errs, fmt.Errorf("marshal bidRequest: %w", err)) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return append(requestsToBidder, &adapters.RequestData{ + Method: http.MethodPost, + Uri: a.endpointURL + "?publisher_id=" + org, + Body: openRTBRequestJSON, + Headers: headers, + }), nil +} + +// MakeBids unpacks the server's response into Bids. +func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + + var errs []error + + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid) + if err != nil { + errs = append(errs, err) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + + return bidResponse, errs +} + +func extractOrg(openRTBRequest *openrtb2.BidRequest) (string, error) { + var err error + for _, imp := range openRTBRequest.Imp { + var bidderExt adapters.ExtImpBidder + if err = json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return "", fmt.Errorf("unmarshal bidderExt: %w", err) + } + + var impExt openrtb_ext.ImpExtRise + if err = json.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + return "", fmt.Errorf("unmarshal ImpExtRise: %w", err) + } + + if impExt.Org != "" { + return strings.TrimSpace(impExt.Org), nil + } + if impExt.PublisherID != "" { + return strings.TrimSpace(impExt.PublisherID), nil + } + } + + return "", errors.New("no org or publisher_id supplied") +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + default: + return "", fmt.Errorf("unsupported MType %d", bid.MType) + } +} diff --git a/adapters/rise/rise_test.go b/adapters/rise/rise_test.go new file mode 100644 index 00000000000..1ba3f8a865d --- /dev/null +++ b/adapters/rise/rise_test.go @@ -0,0 +1,24 @@ +package rise + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const testsDir = "risetest" +const testsBidderEndpoint = "http://localhost/prebid_server" + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderRise, + config.Adapter{Endpoint: testsBidderEndpoint}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, testsDir, bidder) +} diff --git a/adapters/rise/risetest/exemplary/banner-and-video-app.json b/adapters/rise/risetest/exemplary/banner-and-video-app.json new file mode 100644 index 00000000000..fa267977f72 --- /dev/null +++ b/adapters/rise/risetest/exemplary/banner-and-video-app.json @@ -0,0 +1,211 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ], + "app": { + "id": "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA", + "name": "Yahoo Weather", + "bundle": "12345", + "storeurl": "https://itunes.apple.com/id628677149", + "cat": ["IAB15", "IAB15-10"], + "ver": "1.0.2", + "publisher": { + "id": "1" + } + }, + "device": { + "dnt": 0, + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X) AppleWebKit / 534.46(KHTML, like Gecko) Version / 5.1 Mobile / 9 A334 Safari / 7534.48 .3", + "ip": "123.145.167.189", + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "carrier": "VERIZON", + "language": "en", + "make": "Apple", + "model": "iPhone", + "os": "iOS", + "osv": "6.1", + "js": 1, + "connectiontype": 3, + "devicetype": 1 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ], + "app": { + "id": "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA", + "name": "Yahoo Weather", + "bundle": "12345", + "storeurl": "https://itunes.apple.com/id628677149", + "cat": ["IAB15", "IAB15-10"], + "ver": "1.0.2", + "publisher": { + "id": "1" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X) AppleWebKit / 534.46(KHTML, like Gecko) Version / 5.1 Mobile / 9 A334 Safari / 7534.48 .3", + "ip": "123.145.167.189", + "devicetype": 1, + "make": "Apple", + "model": "iPhone", + "os": "iOS", + "osv": "6.1", + "js": 1, + "dnt": 0, + "language": "en", + "carrier": "VERIZON", + "connectiontype": 3, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024, + "mtype": 2 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/rise/risetest/exemplary/banner-and-video-gdpr.json b/adapters/rise/risetest/exemplary/banner-and-video-gdpr.json new file mode 100644 index 00000000000..980b62446b5 --- /dev/null +++ b/adapters/rise/risetest/exemplary/banner-and-video-gdpr.json @@ -0,0 +1,182 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ], + "user": { + "id": "eyJ0ZW1wVUlEcyI6eyJhZGZvcm0iOnsidWlkIjoiMzA5MTMwOTUxNjQ5NDA1MjcxIiwiZXhwaXJlcyI6IjIwMTgtMDYtMjBUMTE6NDA6MzUuODAwNTE0NzQ3KzA1OjMwIn0sImFkbnhzIjp7InVpZCI6IjM1MTUzMjg2MTAyNjMxNjQ0ODQiLCJleHBpcmVzIjoiMjAxOC0wNi0xOFQxODoxMjoxNy4wMTExMzg2MDgrMDU6MzAifX0sImJkYXkiOiIyMDE4LTA2LTA0VDE4OjEyOjE3LjAxMTEzMDg3NSswNTozMCJ9", + "ext": { + "consent": "BOPVK28OPVK28ABABAENA8-AAAADkCNQCGoQAAQ" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ], + "user": { + "id": "eyJ0ZW1wVUlEcyI6eyJhZGZvcm0iOnsidWlkIjoiMzA5MTMwOTUxNjQ5NDA1MjcxIiwiZXhwaXJlcyI6IjIwMTgtMDYtMjBUMTE6NDA6MzUuODAwNTE0NzQ3KzA1OjMwIn0sImFkbnhzIjp7InVpZCI6IjM1MTUzMjg2MTAyNjMxNjQ0ODQiLCJleHBpcmVzIjoiMjAxOC0wNi0xOFQxODoxMjoxNy4wMTExMzg2MDgrMDU6MzAifX0sImJkYXkiOiIyMDE4LTA2LTA0VDE4OjEyOjE3LjAxMTEzMDg3NSswNTozMCJ9", + "ext": { + "consent": "BOPVK28OPVK28ABABAENA8-AAAADkCNQCGoQAAQ" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024, + "mtype": 2 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/rise/risetest/exemplary/banner-and-video-site.json b/adapters/rise/risetest/exemplary/banner-and-video-site.json new file mode 100644 index 00000000000..fda9efdbdfc --- /dev/null +++ b/adapters/rise/risetest/exemplary/banner-and-video-site.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "publisher_id": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "publisher_id": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ], + "site": { + "id": "102855", + "cat": ["IAB3-1"], + "domain": "www.foobar.com", + "page": "http://www.foobar.com/1234.html ", + "publisher": { + "id": "8953", + "name": "foobar.com", + "cat": ["IAB3-1"], + "domain": "foobar.com" + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version / 5.1 .7 Safari / 534.57 .2", + "ip": "123.145.167.10" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "publisher_id": "72721", + "zone": "1r", + "path": "mvo" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "publisher_id": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ], + "site": { + "id": "102855", + "cat": ["IAB3-1"], + "domain": "www.foobar.com", + "page": "http://www.foobar.com/1234.html ", + "publisher": { + "id": "8953", + "name": "foobar.com", + "cat": ["IAB3-1"], + "domain": "foobar.com" + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version / 5.1 .7 Safari / 534.57 .2", + "ip": "123.145.167.10" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024, + "mtype": 2 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576, + "mtype": 2 + }, + "type": "video" + }] + } + ] +} diff --git a/adapters/rise/risetest/exemplary/banner-and-video.json b/adapters/rise/risetest/exemplary/banner-and-video.json new file mode 100644 index 00000000000..d0d6a9bcc44 --- /dev/null +++ b/adapters/rise/risetest/exemplary/banner-and-video.json @@ -0,0 +1,189 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2, + 5 + ], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [ + 2 + ], + "delivery": [ + 1 + ], + "api": [ + 1, + 2, + 3, + 4 + ] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "zone": "1r", + "path": "mvo" + } + } + }, + { + "id": "test-imp-video-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2, + 5 + ], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [ + 2 + ], + "delivery": [ + 1 + ], + "api": [ + 1, + 2, + 3, + 4 + ] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024, + "mtype": 1 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576, + "mtype": 1 + }, + "type": "banner" + }] + } + ] +} diff --git a/adapters/rise/risetest/exemplary/simple-banner-both-ids.json b/adapters/rise/risetest/exemplary/simple-banner-both-ids.json new file mode 100644 index 00000000000..cb6bbfa779f --- /dev/null +++ b/adapters/rise/risetest/exemplary/simple-banner-both-ids.json @@ -0,0 +1,117 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "org": "72720", + "publisher_id": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72720", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "org": "72720", + "publisher_id": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "Rhythmone", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + }] + } + ] +} diff --git a/adapters/rise/risetest/exemplary/simple-banner.json b/adapters/rise/risetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..13e965d1e2f --- /dev/null +++ b/adapters/rise/risetest/exemplary/simple-banner.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "publisher_id": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "publisher_id": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "Rhythmone", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + }] + } + ] +} diff --git a/adapters/rise/risetest/exemplary/simple-video.json b/adapters/rise/risetest/exemplary/simple-video.json new file mode 100644 index 00000000000..921736a6295 --- /dev/null +++ b/adapters/rise/risetest/exemplary/simple-video.json @@ -0,0 +1,110 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "Rhythmone", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576, + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576, + "mtype": 2 + }, + "type": "video" + }] + } + ] +} diff --git a/adapters/rise/risetest/supplemental/bad-request.json b/adapters/rise/risetest/supplemental/bad-request.json new file mode 100644 index 00000000000..b5bedc090d9 --- /dev/null +++ b/adapters/rise/risetest/supplemental/bad-request.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/rise/risetest/supplemental/missing-bidder.json b/adapters/rise/risetest/supplemental/missing-bidder.json new file mode 100644 index 00000000000..0a7cc67ca08 --- /dev/null +++ b/adapters/rise/risetest/supplemental/missing-bidder.json @@ -0,0 +1,35 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-missing-ext-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "maxextended": 30, + "minbitrate": 300, + "maxbitrate": 1500, + "protocols": [1,2,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": {} + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "extractOrg: unmarshal ImpExtRise: unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/rise/risetest/supplemental/missing-extension.json b/adapters/rise/risetest/supplemental/missing-extension.json new file mode 100644 index 00000000000..74e94f45c79 --- /dev/null +++ b/adapters/rise/risetest/supplemental/missing-extension.json @@ -0,0 +1,34 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-missing-ext-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "maxextended": 30, + "minbitrate": 300, + "maxbitrate": 1500, + "protocols": [1,2,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "extractOrg: unmarshal bidderExt: unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/rise/risetest/supplemental/missing-mtype.json b/adapters/rise/risetest/supplemental/missing-mtype.json new file mode 100644 index 00000000000..5693f25ad83 --- /dev/null +++ b/adapters/rise/risetest/supplemental/missing-mtype.json @@ -0,0 +1,118 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server?publisher_id=72721", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "72721", + "zone": "1r", + "path": "mvo" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "Rhythmone", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576 + } + ] + }, + { + "seat": "Rhythmone", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576, + "mtype": 3 + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unsupported MType 0", + "comparison": "literal" + }, + { + "value": "unsupported MType 3", + "comparison": "literal" + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/rise/risetest/supplemental/missing-org.json b/adapters/rise/risetest/supplemental/missing-org.json new file mode 100644 index 00000000000..4107d40e1c3 --- /dev/null +++ b/adapters/rise/risetest/supplemental/missing-org.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-missing-ext-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "maxextended": 30, + "minbitrate": 300, + "maxbitrate": 1500, + "protocols": [1,2,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "org": "", + "publisher_id": "" + }} + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "extractOrg: no org or publisher_id supplied", + "comparison": "literal" + } + ] +} diff --git a/adapters/rtbhouse/rtbhouse.go b/adapters/rtbhouse/rtbhouse.go index c8afb0c4259..dc67df9aa2b 100644 --- a/adapters/rtbhouse/rtbhouse.go +++ b/adapters/rtbhouse/rtbhouse.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/adapters" @@ -12,6 +13,15 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) +const ( + BidderCurrency string = "USD" +) + +// RTBHouseAdapter implements the Bidder interface. +type RTBHouseAdapter struct { + endpoint string +} + // Builder builds a new instance of the RTBHouse adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &RTBHouseAdapter{ @@ -20,11 +30,6 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co return bidder, nil } -// RTBHouseAdapter implements the Bidder interface. -type RTBHouseAdapter struct { - endpoint string -} - // MakeRequests prepares the HTTP requests which should be made to fetch bids. func (adapter *RTBHouseAdapter) MakeRequests( openRTBRequest *openrtb2.BidRequest, @@ -33,7 +38,48 @@ func (adapter *RTBHouseAdapter) MakeRequests( requestsToBidder []*adapters.RequestData, errs []error, ) { - openRTBRequestJSON, err := json.Marshal(openRTBRequest) + + reqCopy := *openRTBRequest + reqCopy.Imp = []openrtb2.Imp{} + for _, imp := range openRTBRequest.Imp { + var bidFloorCur = imp.BidFloorCur + var bidFloor = imp.BidFloor + if bidFloorCur == "" && bidFloor == 0 { + rtbhouseExt, err := getImpressionExt(imp) + if err != nil { + return nil, []error{err} + } + if rtbhouseExt.BidFloor > 0 && len(reqCopy.Cur) > 0 { + bidFloorCur = reqCopy.Cur[0] + bidFloor = rtbhouseExt.BidFloor + } + } + + // Check if imp comes with bid floor amount defined in a foreign currency + if bidFloor > 0 && bidFloorCur != "" && strings.ToUpper(bidFloorCur) != BidderCurrency { + // Convert to US dollars + convertedValue, err := reqInfo.ConvertCurrency(bidFloor, bidFloorCur, BidderCurrency) + if err != nil { + return nil, []error{err} + } + + bidFloorCur = BidderCurrency + bidFloor = convertedValue + } + + if bidFloor > 0 && bidFloorCur == BidderCurrency { + // Update after conversion. All imp elements inside request.Imp are shallow copies + // therefore, their non-pointer values are not shared memory and are safe to modify. + imp.BidFloorCur = bidFloorCur + imp.BidFloor = bidFloor + } + + // Set the CUR of bid to BIDDER_CURRENCY after converting all floors + reqCopy.Cur = []string{BidderCurrency} + reqCopy.Imp = append(reqCopy.Imp, imp) + } + + openRTBRequestJSON, err := json.Marshal(reqCopy) if err != nil { errs = append(errs, err) return nil, errs @@ -52,6 +98,24 @@ func (adapter *RTBHouseAdapter) MakeRequests( return requestsToBidder, errs } +func getImpressionExt(imp openrtb2.Imp) (*openrtb_ext.ExtImpRTBHouse, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Bidder extension not provided or can't be unmarshalled", + } + } + + var rtbhouseExt openrtb_ext.ExtImpRTBHouse + if err := json.Unmarshal(bidderExt.Bidder, &rtbhouseExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error while unmarshaling bidder extension", + } + } + + return &rtbhouseExt, nil +} + const unexpectedStatusCodeFormat = "" + "Unexpected status code: %d. Run with request.debug = 1 for more info" @@ -97,6 +161,8 @@ func (adapter *RTBHouseAdapter) MakeBids( } } + bidderResponse.Currency = BidderCurrency + return bidderResponse, nil } diff --git a/adapters/rtbhouse/rtbhousetest/exemplary/currency-conversion.json b/adapters/rtbhouse/rtbhousetest/exemplary/currency-conversion.json new file mode 100644 index 00000000000..95bfb1e4420 --- /dev/null +++ b/adapters/rtbhouse/rtbhousetest/exemplary/currency-conversion.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "bidfloor": 1.00, + "bidfloorcur": "EUR", + "ext": { + "bidder": { + "placementId": "12345" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid_server", + "body": { + "id": "test-request-id", + "cur": ["USD"], + "imp": [ + { + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "bidfloor": 0.05, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "placementId": "12345" + } + }, + "id": "test-imp-id" + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "cur": "USD", + "seatbid": [{ + "seat": "rtbhouse", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 300, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + } + ] + }] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 300, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + }, + "type": "banner" + } + ] + } + ] + } diff --git a/adapters/rtbhouse/rtbhousetest/exemplary/simple-banner.json b/adapters/rtbhouse/rtbhousetest/exemplary/simple-banner.json index 3a636450f3c..2a8c4681ffa 100644 --- a/adapters/rtbhouse/rtbhousetest/exemplary/simple-banner.json +++ b/adapters/rtbhouse/rtbhousetest/exemplary/simple-banner.json @@ -23,6 +23,7 @@ "uri": "http://localhost/prebid_server", "body": { "id": "test-request-id", + "cur": ["USD"], "site": { "page": "https://good.site/url" }, diff --git a/adapters/rtbhouse/rtbhousetest/supplemental/bad_response.json b/adapters/rtbhouse/rtbhousetest/supplemental/bad_response.json index f84f5555259..e0861fb027d 100644 --- a/adapters/rtbhouse/rtbhousetest/supplemental/bad_response.json +++ b/adapters/rtbhouse/rtbhousetest/supplemental/bad_response.json @@ -26,6 +26,7 @@ "uri": "http://localhost/prebid_server", "body": { "id": "test-request-id", + "cur": ["USD"], "imp": [ { "id": "test-imp-id", diff --git a/adapters/rtbhouse/rtbhousetest/supplemental/currency-missing-conversion.json b/adapters/rtbhouse/rtbhousetest/supplemental/currency-missing-conversion.json new file mode 100644 index 00000000000..d7d65e43e76 --- /dev/null +++ b/adapters/rtbhouse/rtbhousetest/supplemental/currency-missing-conversion.json @@ -0,0 +1,43 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "bidfloor": 1.00, + "bidfloorcur": "JPY", + "ext": { + "bidder": { + "placement": "12345" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "MXN": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Currency conversion rate not found: 'JPY' => 'USD'", + "comparison": "literal" + } + ] + } diff --git a/adapters/rtbhouse/rtbhousetest/supplemental/status_204.json b/adapters/rtbhouse/rtbhousetest/supplemental/status_204.json index 0702c103332..bb7754ec1fc 100644 --- a/adapters/rtbhouse/rtbhousetest/supplemental/status_204.json +++ b/adapters/rtbhouse/rtbhousetest/supplemental/status_204.json @@ -26,6 +26,7 @@ "uri": "http://localhost/prebid_server", "body": { "id": "test-request-id", + "cur": ["USD"], "imp": [ { "id": "test-imp-id", diff --git a/adapters/rtbhouse/rtbhousetest/supplemental/status_400.json b/adapters/rtbhouse/rtbhousetest/supplemental/status_400.json index 65d21406bf0..0b5279326d2 100644 --- a/adapters/rtbhouse/rtbhousetest/supplemental/status_400.json +++ b/adapters/rtbhouse/rtbhousetest/supplemental/status_400.json @@ -26,6 +26,7 @@ "uri": "http://localhost/prebid_server", "body": { "id": "test-request-id", + "cur": ["USD"], "imp": [ { "id": "test-imp-id", diff --git a/adapters/rtbhouse/rtbhousetest/supplemental/status_418.json b/adapters/rtbhouse/rtbhousetest/supplemental/status_418.json index 4c5dd576aa6..6ff3860ee65 100644 --- a/adapters/rtbhouse/rtbhousetest/supplemental/status_418.json +++ b/adapters/rtbhouse/rtbhousetest/supplemental/status_418.json @@ -26,6 +26,7 @@ "uri": "http://localhost/prebid_server", "body": { "id": "test-request-id", + "cur": ["USD"], "imp": [ { "id": "test-imp-id", diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 99072255586..a7ab9ad8f59 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -176,12 +176,6 @@ type rubiconUserExtEidExt struct { Segments []string `json:"segments,omitempty"` } -// defines the contract for bidrequest.user.ext.eids[i].uids[j].ext -type rubiconUserExtEidUidExt struct { - RtiPartner string `json:"rtiPartner,omitempty"` - Stype string `json:"stype"` -} - type mappedRubiconUidsParam struct { segments []string liverampIdl string @@ -940,15 +934,7 @@ func extractUserBuyerUID(eids []openrtb2.EID) string { } for _, uid := range eid.UIDs { - var uidExt rubiconUserExtEidUidExt - err := json.Unmarshal(uid.Ext, &uidExt) - if err != nil { - continue - } - - if uidExt.Stype == "ppuid" || uidExt.Stype == "other" { - return uid.ID - } + return uid.ID } } @@ -1047,8 +1033,11 @@ func resolveNativeObject(native *openrtb2.Native, target map[string]interface{}) return nil, fmt.Errorf("Eventtrackers are not present or not of array type") } - if _, ok := target["context"].(float64); !ok { - return nil, fmt.Errorf("Context is not present or not of int type") + context := target["context"] + if context != nil { + if _, ok := context.(float64); !ok { + return nil, fmt.Errorf("Context is not of int type") + } } if _, ok := target["plcmttype"].(float64); !ok { diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index b665746e1a8..6347415547b 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -123,7 +123,12 @@ func TestResolveNativeObject(t *testing.T) { { nativeObject: openrtb2.Native{Ver: "1", Request: "{\"eventtrackers\": [], \"context\": \"someWrongValue\"}"}, target: map[string]interface{}{}, - expectedError: fmt.Errorf("Context is not present or not of int type"), + expectedError: fmt.Errorf("Context is not of int type"), + }, + { + nativeObject: openrtb2.Native{Ver: "1", Request: "{\"eventtrackers\": [], \"plcmttype\": 2}"}, + target: map[string]interface{}{}, + expectedError: nil, }, { nativeObject: openrtb2.Native{Ver: "1", Request: "{\"eventtrackers\": [], \"context\": 1}"}, @@ -290,12 +295,12 @@ type mockCurrencyConversion struct { mock.Mock } -func (m mockCurrencyConversion) GetRate(from string, to string) (float64, error) { +func (m *mockCurrencyConversion) GetRate(from string, to string) (float64, error) { args := m.Called(from, to) return args.Get(0).(float64), args.Error(1) } -func (m mockCurrencyConversion) GetRates() *map[string]map[string]float64 { +func (m *mockCurrencyConversion) GetRates() *map[string]map[string]float64 { args := m.Called() return args.Get(0).(*map[string]map[string]float64) } diff --git a/adapters/sa_lunamedia/params_test.go b/adapters/sa_lunamedia/params_test.go index bf7a1f493e6..070c97741b0 100644 --- a/adapters/sa_lunamedia/params_test.go +++ b/adapters/sa_lunamedia/params_test.go @@ -9,7 +9,6 @@ import ( var validParams = []string{ `{ "key": "2", "type": "network"}`, - `{ "key": "1"}`, `{ "key": "33232", "type": "publisher"}`, } @@ -36,6 +35,7 @@ var invalidParams = []string{ `{ "anyparam": "anyvalue" }`, `{ "type": "network" }`, `{ "key": "asddsfd", "type": "any"}`, + `{ "key": "1"}`, } func TestInvalidParams(t *testing.T) { diff --git a/adapters/screencore/params_test.go b/adapters/screencore/params_test.go new file mode 100644 index 00000000000..faa2f854928 --- /dev/null +++ b/adapters/screencore/params_test.go @@ -0,0 +1,50 @@ +package screencore + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{ "accountId": "hash", "placementId": "hash"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderScreencore, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Screencore params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `5.2`, + `[]`, + `{}`, + `{"adCode": "string", "seatCode": 5, "originalPublisherid": "string"}`, + `{ "placementId": "", "accountId": "" }`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderScreencore, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/screencore/screencore.go b/adapters/screencore/screencore.go new file mode 100644 index 00000000000..668e8b5e131 --- /dev/null +++ b/adapters/screencore/screencore.go @@ -0,0 +1,176 @@ +package screencore + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + return bidder, nil +} + +func getHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("User-Agent", request.Device.UA) + } + + if len(request.Device.IPv6) > 0 { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + } + + return headers +} + +func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { + if len(openRTBRequest.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "Empty Imp array in BidRequest", + }} + } + + screencoreExt, err := getImpressionExt(&openRTBRequest.Imp[0]) + if err != nil { + return nil, []error{err} + } + + openRTBRequest.Imp[0].Ext = nil + + url, err := a.buildEndpointURL(screencoreExt) + if err != nil { + return nil, []error{err} + } + + reqJSON, err := json.Marshal(openRTBRequest) + if err != nil { + return nil, []error{err} + } + + return []*adapters.RequestData{{ + Method: http.MethodPost, + Body: reqJSON, + Uri: url, + Headers: getHeaders(openRTBRequest), + }}, nil +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtScreencore, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing screencoreExt - " + err.Error(), + } + } + var screencoreExt openrtb_ext.ExtScreencore + if err := json.Unmarshal(bidderExt.Bidder, &screencoreExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing bidderExt - " + err.Error(), + } + } + + return &screencoreExt, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtScreencore) (string, error) { + endpointParams := macros.EndpointTemplateParams{AccountID: params.AccountID, SourceId: params.PlacementID} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func checkResponseStatusCodes(response *adapters.ResponseData) error { + if response.StatusCode == http.StatusServiceUnavailable { + return &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Something went wrong Status Code: [ %d ] ", response.StatusCode), + } + } + + return adapters.CheckResponseStatusCodeForErrors(response) +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + httpStatusError := checkResponseStatusCodes(bidderRawResponse) + if httpStatusError != nil { + return nil, []error{httpStatusError} + } + + responseBody := bidderRawResponse.Body + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bad Server Response", + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + var bidsArray []*adapters.TypedBid + + for _, sb := range bidResp.SeatBid { + for idx, bid := range sb.Bid { + bidType, err := getMediaTypeForImp(bid) + if err != nil { + return nil, []error{err} + } + + bidsArray = append(bidsArray, &adapters.TypedBid{ + Bid: &sb.Bid[idx], + BidType: bidType, + }) + } + } + + bidResponse.Bids = bidsArray + return bidResponse, nil +} + +func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("unsupported MType %d", bid.MType) + } +} diff --git a/adapters/screencore/screencore_test.go b/adapters/screencore/screencore_test.go new file mode 100644 index 00000000000..6ee7f3ad554 --- /dev/null +++ b/adapters/screencore/screencore_test.go @@ -0,0 +1,20 @@ +package screencore + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderScreencore, config.Adapter{ + Endpoint: "http://h1.screencore.io/?kp={{.AccountID}}&kn={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "screencoretest", bidder) +} diff --git a/adapters/screencore/screencoretest/exemplary/banner-app.json b/adapters/screencore/screencoretest/exemplary/banner-app.json new file mode 100644 index 00000000000..0b75398d767 --- /dev/null +++ b/adapters/screencore/screencoretest/exemplary/banner-app.json @@ -0,0 +1,152 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/exemplary/banner-web.json b/adapters/screencore/screencoretest/exemplary/banner-web.json new file mode 100644 index 00000000000..a9eed6bfb53 --- /dev/null +++ b/adapters/screencore/screencoretest/exemplary/banner-web.json @@ -0,0 +1,197 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + }, + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/exemplary/native-app.json b/adapters/screencore/screencoretest/exemplary/native-app.json new file mode 100644 index 00000000000..eece64eea2d --- /dev/null +++ b/adapters/screencore/screencoretest/exemplary/native-app.json @@ -0,0 +1,148 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "type": "native", + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/exemplary/native-web.json b/adapters/screencore/screencoretest/exemplary/native-web.json new file mode 100644 index 00000000000..5878519f33f --- /dev/null +++ b/adapters/screencore/screencoretest/exemplary/native-web.json @@ -0,0 +1,135 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "2607:fb90:f27:4512:d800:cb23:a603:e245" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/exemplary/video-app.json b/adapters/screencore/screencoretest/exemplary/video-app.json new file mode 100644 index 00000000000..7368af33a98 --- /dev/null +++ b/adapters/screencore/screencoretest/exemplary/video-app.json @@ -0,0 +1,161 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2 + } + ], + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 1280, + "h": 720, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/exemplary/video-web.json b/adapters/screencore/screencoretest/exemplary/video-web.json new file mode 100644 index 00000000000..a56ddb78bea --- /dev/null +++ b/adapters/screencore/screencoretest/exemplary/video-web.json @@ -0,0 +1,159 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + }, + "mtype": 2 + } + ], + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + }, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/bad_media_type.json b/adapters/screencore/screencoretest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..ed7200c54a6 --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/bad_media_type.json @@ -0,0 +1,135 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "test-imp-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50 + } + ], + "mtype": 0, + "seat": "screencore" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unsupported MType 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/empty-imp-array.json b/adapters/screencore/screencoretest/supplemental/empty-imp-array.json new file mode 100644 index 00000000000..e19996fbe2a --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/empty-imp-array.json @@ -0,0 +1,16 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Empty Imp array in BidRequest", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] + } \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/empty-seatbid-array.json b/adapters/screencore/screencoretest/supplemental/empty-seatbid-array.json new file mode 100644 index 00000000000..0ed6159a54a --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/empty-seatbid-array.json @@ -0,0 +1,130 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "mockResponse": { + "status": 200, + "body": "invalid response" + }, + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/invalid-bidder-ext-object.json b/adapters/screencore/screencoretest/supplemental/invalid-bidder-ext-object.json new file mode 100644 index 00000000000..4dc5c5a62bc --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/invalid-bidder-ext-object.json @@ -0,0 +1,33 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt - json: cannot unmarshal string into Go value of type openrtb_ext.ExtScreencore", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": "wrongBidderExt" + } + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/invalid-response.json b/adapters/screencore/screencoretest/supplemental/invalid-response.json new file mode 100644 index 00000000000..b2417d9d8d6 --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/invalid-response.json @@ -0,0 +1,112 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bad Server Response", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/invalid-screencore-ext-object.json b/adapters/screencore/screencoretest/supplemental/invalid-screencore-ext-object.json new file mode 100644 index 00000000000..b52e18fab50 --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/invalid-screencore-ext-object.json @@ -0,0 +1,31 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing screencoreExt - json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "wrongscreencoreExt" + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/status-code-bad-request.json b/adapters/screencore/screencoretest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..68deeef92de --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/status-code-bad-request.json @@ -0,0 +1,93 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/status-code-no-content.json b/adapters/screencore/screencoretest/supplemental/status-code-no-content.json new file mode 100644 index 00000000000..1b6fa699a1b --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/status-code-no-content.json @@ -0,0 +1,76 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/status-code-other-error.json b/adapters/screencore/screencoretest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..994b33f2b80 --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/status-code-other-error.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 306 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 306. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/screencore/screencoretest/supplemental/status-code-service-unavailable.json b/adapters/screencore/screencoretest/supplemental/status-code-service-unavailable.json new file mode 100644 index 00000000000..b885df20c58 --- /dev/null +++ b/adapters/screencore/screencoretest/supplemental/status-code-service-unavailable.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "placementId": "placementId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://h1.screencore.io/?kp=accountId&kn=placementId", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Something went wrong Status Code: [ 503 ] ", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/sharethrough/sharethrough.go b/adapters/sharethrough/sharethrough.go index 70045cc7bf5..009ce0aa1d5 100644 --- a/adapters/sharethrough/sharethrough.go +++ b/adapters/sharethrough/sharethrough.go @@ -28,7 +28,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co return bidder, nil } -func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var requests []*adapters.RequestData var errors []error @@ -83,27 +83,35 @@ func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.Ex requestCopy.BCat = append(requestCopy.BCat, strImpParams.BCat...) requestCopy.BAdv = append(requestCopy.BAdv, strImpParams.BAdv...) - requestCopy.Imp = []openrtb2.Imp{imp} - - requestJSON, err := json.Marshal(requestCopy) + impressionsByMediaType, err := splitImpressionsByMediaType(&imp) if err != nil { errors = append(errors, err) continue } - requestData := &adapters.RequestData{ - Method: "POST", - Uri: a.endpoint, - Body: requestJSON, - Headers: headers, + for _, impression := range impressionsByMediaType { + requestCopy.Imp = []openrtb2.Imp{impression} + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errors = append(errors, err) + continue + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJSON, + Headers: headers, + } + requests = append(requests, requestData) } - requests = append(requests, requestData) } return requests, errors } -func (a adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { return nil, nil } @@ -130,20 +138,71 @@ func (a adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.Re bidderResponse := adapters.NewBidderResponse() bidderResponse.Currency = "USD" + var errors []error for _, seatBid := range bidResp.SeatBid { for i := range seatBid.Bid { - bidType := openrtb_ext.BidTypeBanner - if bidReq.Imp[0].Video != nil { - bidType = openrtb_ext.BidTypeVideo + bid := &seatBid.Bid[i] + bidType, err := getMediaTypeForBid(*bid) + if err != nil { + errors = append(errors, err) } bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ BidType: bidType, - Bid: &seatBid.Bid[i], + Bid: bid, }) } } - return bidderResponse, nil + return bidderResponse, errors +} + +func splitImpressionsByMediaType(impression *openrtb2.Imp) ([]openrtb2.Imp, error) { + if impression.Banner == nil && impression.Video == nil && impression.Native == nil { + return nil, &errortypes.BadInput{Message: "Invalid MediaType. Sharethrough only supports Banner, Video and Native."} + } + + if impression.Audio != nil { + impression.Audio = nil + } + + impressions := make([]openrtb2.Imp, 0, 3) + + if impression.Banner != nil { + impCopy := *impression + impCopy.Video = nil + impCopy.Native = nil + impressions = append(impressions, impCopy) + } + + if impression.Video != nil { + impCopy := *impression + impCopy.Banner = nil + impCopy.Native = nil + impressions = append(impressions, impCopy) + } + + if impression.Native != nil { + impression.Banner = nil + impression.Video = nil + impressions = append(impressions, *impression) + } + + return impressions, nil +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse bid mediatype for impression \"%s\"", bid.ImpID), + } } diff --git a/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json b/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json index 746c72f7125..29f1b1d1eb1 100644 --- a/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json +++ b/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json @@ -37,12 +37,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-banner", @@ -100,7 +96,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 250 + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } } ] } @@ -121,7 +122,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 250 + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } }, "type": "banner" } diff --git a/adapters/sharethrough/sharethroughtest/exemplary/app-native.json b/adapters/sharethrough/sharethroughtest/exemplary/app-native.json new file mode 100644 index 00000000000..ff283d9ceb0 --- /dev/null +++ b/adapters/sharethrough/sharethroughtest/exemplary/app-native.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "web-native", + "imp": [ + { + "id": "native-imp-id", + "ext": { + "bidder": { + "pkey": "pkey" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-native", + "imp": [ + { + "id": "native-imp-id", + "tagid": "pkey", + "ext": { + "bidder": { + "pkey": "pkey" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ], + "source": { + "ext": { + "version": "", + "str": "10.0" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-native", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-native", + "impid": "native-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-native", + "impid": "native-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/sharethrough/sharethroughtest/exemplary/app-video.json b/adapters/sharethrough/sharethroughtest/exemplary/app-video.json index 240b94aac13..e8e3d97e152 100644 --- a/adapters/sharethrough/sharethroughtest/exemplary/app-video.json +++ b/adapters/sharethrough/sharethroughtest/exemplary/app-video.json @@ -11,21 +11,14 @@ }, "bidder": { "pkey": "pkey", - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "video": { "w": 640, "h": 480, - "mimes": [ - "video/mp4" - ], + "mimes": ["video/mp4"], "placement": 1 } } @@ -47,12 +40,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-video", @@ -67,21 +56,14 @@ }, "bidder": { "pkey": "pkey", - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "video": { "w": 640, "h": 480, - "mimes": [ - "video/mp4" - ], + "mimes": ["video/mp4"], "placement": 1 } } @@ -122,7 +104,12 @@ "adm": "TAG", "price": 20, "w": 640, - "h": 480 + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } } ] } @@ -143,7 +130,12 @@ "adm": "TAG", "price": 20, "w": 640, - "h": 480 + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } }, "type": "video" } diff --git a/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json b/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json index b35749654a4..cbc5630aa4e 100644 --- a/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json +++ b/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json @@ -38,12 +38,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-banner", @@ -101,7 +97,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 250 + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } } ] } @@ -122,7 +123,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 250 + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } }, "type": "banner" } diff --git a/adapters/sharethrough/sharethroughtest/exemplary/web-video.json b/adapters/sharethrough/sharethroughtest/exemplary/web-video.json index 02de1780f79..892d140df3a 100644 --- a/adapters/sharethrough/sharethroughtest/exemplary/web-video.json +++ b/adapters/sharethrough/sharethroughtest/exemplary/web-video.json @@ -11,21 +11,14 @@ }, "bidder": { "pkey": "pkey", - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "video": { "w": 640, "h": 480, - "mimes": [ - "video/mp4" - ], + "mimes": ["video/mp4"], "placement": 1 } } @@ -48,12 +41,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-video", @@ -68,21 +57,14 @@ }, "bidder": { "pkey": "pkey", - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "video": { "w": 640, "h": 480, - "mimes": [ - "video/mp4" - ], + "mimes": ["video/mp4"], "placement": 1 } } @@ -123,7 +105,12 @@ "adm": "TAG", "price": 20, "w": 640, - "h": 480 + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } } ] } @@ -144,7 +131,12 @@ "adm": "TAG", "price": 20, "w": 640, - "h": 480 + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } }, "type": "video" } diff --git a/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json b/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json index 82908095edb..76d8178069c 100644 --- a/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json +++ b/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json @@ -90,9 +90,6 @@ } } ], - "expectedBidResponses": [ - null - ], "expectedMakeBidsErrors": [ { "value": "Unexpected status code: 400.", diff --git a/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json b/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json index 11cbc3a44d5..5562c466c10 100644 --- a/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json +++ b/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json @@ -90,9 +90,6 @@ } } ], - "expectedBidResponses": [ - null - ], "expectedMakeBidsErrors": [ { "value": "unexpected status code: 500.", diff --git a/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json b/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json index e083c798c6a..6007c63c497 100644 --- a/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json +++ b/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json @@ -53,12 +53,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-banner", @@ -116,7 +112,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 250 + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } } ] } @@ -128,12 +129,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-banner", @@ -191,7 +188,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 600 + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } } ] } @@ -212,7 +214,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 250 + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } }, "type": "banner" } @@ -229,7 +236,12 @@ "adm": "
Ad
", "price": 20, "w": 300, - "h": 600 + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } }, "type": "banner" } diff --git a/adapters/sharethrough/sharethroughtest/supplemental/multiformat-impression.json b/adapters/sharethrough/sharethroughtest/supplemental/multiformat-impression.json new file mode 100644 index 00000000000..fcebf79acc8 --- /dev/null +++ b/adapters/sharethrough/sharethroughtest/supplemental/multiformat-impression.json @@ -0,0 +1,362 @@ +{ + "mockBidRequest": { + "id": "parent-id", + "tmax": 3000, + "imp": [ + { + "id": "impression-id", + "ext": { + "bidder": { + "pkey": "pkey1" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + }, + "audio": { + "mimes": [ + "audio/mp4" + ], + "protocols": [ + 1, + 2 + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "parent-id", + "tmax": 3000, + "imp": [ + { + "id": "impression-id", + "tagid": "pkey1", + "ext": { + "bidder": { + "pkey": "pkey1" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + }, + "source": { + "ext": { + "version": "", + "str": "10.0" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "parent-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "parent-id", + "impid": "impression-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "http://whatever.url", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "parent-id", + "tmax": 3000, + "imp": [ + { + "id": "impression-id", + "tagid": "pkey1", + "ext": { + "bidder": { + "pkey": "pkey1" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + }, + "source": { + "ext": { + "version": "", + "str": "10.0" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "parent-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "parent-id", + "impid": "impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "http://whatever.url", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "parent-id", + "tmax": 3000, + "imp": [ + { + "id": "impression-id", + "tagid": "pkey1", + "ext": { + "bidder": { + "pkey": "pkey1" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + }, + "source": { + "ext": { + "version": "", + "str": "10.0" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "parent-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "parent-id", + "impid": "impression-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "native" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "parent-id", + "impid": "impression-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "parent-id", + "impid": "impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "parent-id", + "impid": "impression-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json b/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json index 6d7dd9390aa..3b8dd9973b2 100644 --- a/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json +++ b/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json @@ -90,8 +90,5 @@ } } ], - "expectedBidResponses": [ - null - ], "expectedMakeBidsErrors": [] } diff --git a/adapters/sharethrough/sharethroughtest/supplemental/schain.json b/adapters/sharethrough/sharethroughtest/supplemental/schain.json index 62b0e43cac9..55a329d31fd 100644 --- a/adapters/sharethrough/sharethroughtest/supplemental/schain.json +++ b/adapters/sharethrough/sharethroughtest/supplemental/schain.json @@ -26,21 +26,14 @@ }, "bidder": { "pkey": "pkey", - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "video": { "w": 640, "h": 480, - "mimes": [ - "video/mp4" - ], + "mimes": ["video/mp4"], "placement": 1 } } @@ -63,12 +56,8 @@ "expectedRequest": { "uri": "http://whatever.url", "headers": { - "Content-Type": [ - "application/json;charset=utf-8" - ], - "Accept": [ - "application/json" - ] + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] }, "body": { "id": "web-video-schain", @@ -83,21 +72,14 @@ }, "bidder": { "pkey": "pkey", - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "video": { "w": 640, "h": 480, - "mimes": [ - "video/mp4" - ], + "mimes": ["video/mp4"], "placement": 1 } } @@ -130,13 +112,8 @@ "str": "10.0" } }, - "badv": [ - "ford.com" - ], - "bcat": [ - "IAB-1", - "IAB-2" - ] + "badv": ["ford.com"], + "bcat": ["IAB-1", "IAB-2"] } }, "mockResponse": { @@ -154,7 +131,12 @@ "adm": "TAG", "price": 20, "w": 640, - "h": 480 + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } } ] } @@ -175,7 +157,12 @@ "adm": "TAG", "price": 20, "w": 640, - "h": 480 + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } }, "type": "video" } diff --git a/adapters/silverpush/devicetype.go b/adapters/silverpush/devicetype.go new file mode 100644 index 00000000000..a44464048c2 --- /dev/null +++ b/adapters/silverpush/devicetype.go @@ -0,0 +1,46 @@ +package silverpush + +import ( + "regexp" + "strings" + + "github.com/prebid/openrtb/v19/openrtb2" +) + +var isValidMobile = regexp.MustCompile(`(ios|ipod|ipad|iphone|android)`) +var isCtv = regexp.MustCompile(`(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)`) +var isIos = regexp.MustCompile(`(iPhone|iPod|iPad)`) + +func isMobile(ua string) bool { + return isValidMobile.MatchString(strings.ToLower(ua)) +} + +func isCTV(ua string) bool { + return isCtv.MatchString(strings.ToLower(ua)) +} + +// isValidEids checks for valid eids. +func isValidEids(eids []openrtb2.EID) bool { + for i := 0; i < len(eids); i++ { + if len(eids[i].UIDs) > 0 && eids[i].UIDs[0].ID != "" { + return true + } + } + return false +} + +func getOS(ua string) string { + if strings.Contains(ua, "Windows") { + return "Windows" + } else if isIos.MatchString(ua) { + return "iOS" + } else if strings.Contains(ua, "Mac OS X") { + return "macOS" + } else if strings.Contains(ua, "Android") { + return "Android" + } else if strings.Contains(ua, "Linux") { + return "Linux" + } else { + return "Unknown" + } +} diff --git a/adapters/silverpush/params_test.go b/adapters/silverpush/params_test.go new file mode 100644 index 00000000000..8003b49ac3d --- /dev/null +++ b/adapters/silverpush/params_test.go @@ -0,0 +1,65 @@ +package silverpush + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file intends to test static/bidder-params/silverpush.json + +// These also validate the format of the external API: request.imp[i].bidRequestExt.silverpush + +// TestValidParams makes sure that the Smaato schema accepts all imp.bidRequestExt fields which Smaato supports. +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderSilverPush, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected silverpush params: %s \n Error: %s", validParam, err) + } + } +} + +// TestInvalidParams makes sure that the Smaato schema rejects all the imp.bidRequestExt fields which are not support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderSilverPush, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"publisherId":"test-id-1234-silverpush","bidfloor": 0.05}`, + `{"publisherId":"test123","bidfloor": 0.05}`, + `{"publisherId":"testSIlverpush","bidfloor": 0.05}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"bidfloor": "1123581321"}`, + `{"publisherId":false}`, + `{"bidfloor":false}`, + `{"publisherId":0,"bidfloor": 1123581321}`, + `{"publisherId":false,"bidfloor": true}`, + `{"instl": 0}`, + `{"secure": 0}`, + `{"bidfloor": "1123581321"}`, + `{"publisherId":{}}`, +} diff --git a/adapters/silverpush/silverpush.go b/adapters/silverpush/silverpush.go new file mode 100644 index 00000000000..b5726cb28b7 --- /dev/null +++ b/adapters/silverpush/silverpush.go @@ -0,0 +1,327 @@ +package silverpush + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + bidderConfig = "sp_pb_ortb" + bidderVersion = "1.0.0" +) + +type adapter struct { + bidderName string + endpoint string +} + +type SilverPushImpExt map[string]json.RawMessage + +type SilverPushReqExt struct { + PublisherId string `json:"publisherId"` + BidFloor float64 `json:"bidfloor"` +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + imps := request.Imp + var errors []error + requests := make([]*adapters.RequestData, 0, len(imps)) + + for _, imp := range imps { + impsByMediaType := impressionByMediaType(&imp) + + request.Imp = []openrtb2.Imp{impsByMediaType} + + if err := validateRequest(request); err != nil { + errors = append(errors, err) + continue + } + + requestData, err := a.makeRequest(request) + if err != nil { + errors = append(errors, err) + continue + } + + requests = append(requests, requestData) + + } + return requests, errors +} + +func (a *adapter) makeRequest(req *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(req) + if err != nil { + return nil, err + } + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }, nil +} + +func validateRequest(req *openrtb2.BidRequest) error { + imp := &req.Imp[0] + var silverPushExt openrtb_ext.ImpExtSilverpush + if err := setPublisherId(req, imp, &silverPushExt); err != nil { + return err + } + if err := setUser(req); err != nil { + return err + } + + setDevice(req) + + if err := setExtToRequest(req, silverPushExt.PublisherId); err != nil { + return err + } + return setImpForAdExchange(imp, &silverPushExt) +} + +func setDevice(req *openrtb2.BidRequest) { + if req.Device == nil { + return + } + deviceCopy := *req.Device + if len(deviceCopy.UA) == 0 { + return + } + deviceCopy.OS = getOS(deviceCopy.UA) + if isMobile(deviceCopy.UA) { + deviceCopy.DeviceType = 1 + } else if isCTV(deviceCopy.UA) { + deviceCopy.DeviceType = 3 + } else { + deviceCopy.DeviceType = 2 + } + + req.Device = &deviceCopy +} + +func setUser(req *openrtb2.BidRequest) error { + var extUser openrtb_ext.ExtUser + var userExtRaw map[string]json.RawMessage + + if req.User != nil && req.User.Ext != nil { + if err := json.Unmarshal(req.User.Ext, &userExtRaw); err != nil { + return &errortypes.BadInput{Message: "Invalid user.ext."} + } + if userExtDataRaw, ok := userExtRaw["data"]; ok { + if err := json.Unmarshal(userExtDataRaw, &extUser); err != nil { + return &errortypes.BadInput{Message: "Invalid user.ext.data."} + } + var userCopy = *req.User + if isValidEids(extUser.Eids) { + userExt, err := json.Marshal( + &openrtb2.User{ + EIDs: extUser.Eids, + }) + if err != nil { + return &errortypes.BadInput{Message: "Error in marshaling user.eids."} + } + + userCopy.Ext = userExt + req.User = &userCopy + } + } + } + return nil +} + +func setExtToRequest(req *openrtb2.BidRequest, publisherID string) error { + record := map[string]string{ + "bc": bidderConfig + "_" + bidderVersion, + "publisherId": publisherID, + } + reqExt, err := json.Marshal(record) + if err != nil { + return err + } + req.Ext = reqExt + return nil +} + +func setImpForAdExchange(imp *openrtb2.Imp, impExt *openrtb_ext.ImpExtSilverpush) error { + if impExt.BidFloor == 0 { + if imp.Banner != nil { + imp.BidFloor = 0.05 + } else if imp.Video != nil { + imp.BidFloor = 0.1 + } + } else { + imp.BidFloor = impExt.BidFloor + } + + if imp.Banner != nil { + bannerCopy, err := setBannerDimension(imp.Banner) + if err != nil { + return err + } + imp.Banner = bannerCopy + } + + if imp.Video != nil { + videoCopy, err := checkVideoDimension(imp.Video) + if err != nil { + return err + } + imp.Video = videoCopy + } + + return nil +} + +func checkVideoDimension(video *openrtb2.Video) (*openrtb2.Video, error) { + videoCopy := *video + if videoCopy.MaxDuration == 0 { + videoCopy.MaxDuration = 120 + } + if videoCopy.MaxDuration < videoCopy.MinDuration { + videoCopy.MaxDuration = videoCopy.MinDuration + videoCopy.MinDuration = 0 + } + if videoCopy.API == nil || videoCopy.MIMEs == nil || videoCopy.Protocols == nil || videoCopy.MinDuration < 0 { + return nil, &errortypes.BadInput{Message: "Invalid or missing video field(s)"} + } + return &videoCopy, nil +} + +func setBannerDimension(banner *openrtb2.Banner) (*openrtb2.Banner, error) { + if banner.W != nil && banner.H != nil { + return banner, nil + } + if len(banner.Format) == 0 { + return banner, &errortypes.BadInput{Message: "No sizes provided for Banner."} + } + bannerCopy := *banner + bannerCopy.W = openrtb2.Int64Ptr(banner.Format[0].W) + bannerCopy.H = openrtb2.Int64Ptr(banner.Format[0].H) + + return &bannerCopy, nil +} + +func setPublisherId(req *openrtb2.BidRequest, imp *openrtb2.Imp, impExt *openrtb_ext.ImpExtSilverpush) error { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + if err := json.Unmarshal(bidderExt.Bidder, impExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + if impExt.PublisherId == "" { + return &errortypes.BadInput{Message: "Missing publisherId parameter."} + } + if req.Site != nil { + siteCopy := *req.Site + if siteCopy.Publisher == nil { + siteCopy.Publisher = &openrtb2.Publisher{ID: impExt.PublisherId} + } else { + publisher := *siteCopy.Publisher + publisher.ID = impExt.PublisherId + siteCopy.Publisher = &publisher + } + req.Site = &siteCopy + + } else if req.App != nil { + appCopy := *req.App + if appCopy.Publisher == nil { + appCopy.Publisher = &openrtb2.Publisher{ID: impExt.PublisherId} + } else { + publisher := *appCopy.Publisher + publisher.ID = impExt.PublisherId + appCopy.Publisher = &publisher + } + appCopy.Publisher = &openrtb2.Publisher{ID: impExt.PublisherId} + req.App = &appCopy + + } + + return nil +} + +func impressionByMediaType(imp *openrtb2.Imp) openrtb2.Imp { + impCopy := *imp + if imp.Banner != nil { + impCopy.Video = nil + } + if imp.Video != nil { + impCopy.Banner = nil + + } + return impCopy +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp)) + + // overrride default currency + if bidResp.Cur != "" { + bidResponse.Currency = bidResp.Cur + } + + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: getMediaTypeForImp(sb.Bid[i]), + }) + } + } + return bidResponse, nil +} + +// getMediaTypeForImp figures out which media type this bid is for. +// SilverPush doesn't support multi-type impressions. +// If both banner and video exist, take banner as we do not want in-banner video. +func getMediaTypeForImp(bid openrtb2.Bid) openrtb_ext.BidType { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo + default: + return "" + } +} + +// Builder builds a new instance of the silverpush adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + bidderName: string(bidderName), + } + return bidder, nil +} diff --git a/adapters/silverpush/silverpush_test.go b/adapters/silverpush/silverpush_test.go new file mode 100644 index 00000000000..87aa2867061 --- /dev/null +++ b/adapters/silverpush/silverpush_test.go @@ -0,0 +1,59 @@ +package silverpush + +import ( + "encoding/json" + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderSilverPush, config.Adapter{ + Endpoint: "http://localhost:8080/bidder/?identifier=5krH8Q"}, config.Server{}) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + adapterstest.RunJSONBidderTest(t, "silverpushtest", bidder) +} + +func TestResponseWithCurrencies(t *testing.T) { + assertCurrencyInBidResponse(t, "USD", nil) + + currency := "USD" + assertCurrencyInBidResponse(t, "USD", ¤cy) + currency = "EUR" + assertCurrencyInBidResponse(t, "EUR", ¤cy) +} + +func assertCurrencyInBidResponse(t *testing.T, expectedCurrency string, currency *string) { + bidder, buildErr := Builder(openrtb_ext.BidderOpenx, config.Adapter{ + Endpoint: "http://rtb.openx.net/prebid"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + prebidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{}, + } + mockedBidResponse := &openrtb2.BidResponse{} + if currency != nil { + mockedBidResponse.Cur = *currency + } + body, _ := json.Marshal(mockedBidResponse) + responseData := &adapters.ResponseData{ + StatusCode: 200, + Body: body, + } + bidResponse, errs := bidder.MakeBids(prebidRequest, nil, responseData) + + if errs != nil { + t.Fatalf("Failed to make bids %v", errs) + } + assert.Equal(t, expectedCurrency, bidResponse.Currency) +} diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-bidfloor-zero.json b/adapters/silverpush/silverpushtest/exemplary/banner-bidfloor-zero.json new file mode 100644 index 00000000000..a4d73374a42 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-bidfloor-zero.json @@ -0,0 +1,255 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123" + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123" + + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-device-ctv-.json b/adapters/silverpush/silverpushtest/exemplary/banner-device-ctv-.json new file mode 100644 index 00000000000..037b844c519 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-device-ctv-.json @@ -0,0 +1,257 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (X11; smarttv Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Linux", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (X11; smarttv Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-device-site.json b/adapters/silverpush/silverpushtest/exemplary/banner-device-site.json new file mode 100644 index 00000000000..f12af4b3738 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-device-site.json @@ -0,0 +1,257 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Linux", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-eids.json b/adapters/silverpush/silverpushtest/exemplary/banner-eids.json new file mode 100644 index 00000000000..62273801292 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-eids.json @@ -0,0 +1,285 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + "data": { + "eids": [{ + "source":"pubcid.org", + "uids":[ + { + "id":"01EAJWWNEPN3CYMM5N8M5VXY22", + "atype":1 + } + ] + }] + } + + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + + "eids": [{ + "source":"pubcid.org", + "uids":[ + { + "id":"01EAJWWNEPN3CYMM5N8M5VXY22", + "atype":1 + } + ] + }] + + + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-iOS-ua.json b/adapters/silverpush/silverpushtest/exemplary/banner-iOS-ua.json new file mode 100644 index 00000000000..4a8868ce7fa --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-iOS-ua.json @@ -0,0 +1,257 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (iPhone12,1; U; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/15E148 Safari/602.1", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "iOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (iPhone12,1; U; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/15E148 Safari/602.1", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-windows-ua.json b/adapters/silverpush/silverpushtest/exemplary/banner-windows-ua.json new file mode 100644 index 00000000000..7e6eb647c27 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-windows-ua.json @@ -0,0 +1,257 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; RM-1152) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15254", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Windows", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; RM-1152) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15254", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-without-site-publisher.json b/adapters/silverpush/silverpushtest/exemplary/banner-without-site-publisher.json new file mode 100644 index 00000000000..042dd9c5021 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-without-site-publisher.json @@ -0,0 +1,244 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "site": { + "domain": "stg.betterbutter.in" + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner-without-w-h.json b/adapters/silverpush/silverpushtest/exemplary/banner-without-w-h.json new file mode 100644 index 00000000000..a3764658c03 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner-without-w-h.json @@ -0,0 +1,278 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }] + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 290, + "h": 260, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }] + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner.json b/adapters/silverpush/silverpushtest/exemplary/banner.json new file mode 100644 index 00000000000..8476f805716 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner.json @@ -0,0 +1,257 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/banner_without_publisher.json b/adapters/silverpush/silverpushtest/exemplary/banner_without_publisher.json new file mode 100644 index 00000000000..2fd4188f4a5 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/banner_without_publisher.json @@ -0,0 +1,242 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/video-bidfloor-zero.json b/adapters/silverpush/silverpushtest/exemplary/video-bidfloor-zero.json new file mode 100644 index 00000000000..d9650cd0e8a --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/video-bidfloor-zero.json @@ -0,0 +1,247 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + "mimes": [ + "video/mp4" + ], + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + "minduration": 1, + "maxduration": 0, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123" + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + "mimes": [ + "video/mp4" + ], + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + "minduration": 1, + "maxduration": 120, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloor": 0.1, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123" + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "iJQDNZRA", + "impid": "1", + "price": 0.04, + "burl": "https://localhost:8181/tracker/?p=iEUrKGOlRpwuy6LMCoEc8oew34TcCljlTEjAN1URYhXSxosy0IanKUQw36mX8Igm5v5887kijBb_kwdRT8OIJv1KQWLtEYJFkSJOdZJvSFNF_6Ir6_yaslz3Br4JuIUC9owtY46bQy0ltarrzOf8Xhq5XosFj88AVue5y5aBCxvdNF0nqtk1f_IN5h59yNBlUGTQ6_hAxOO_f27g3t1sjDpoojfZ5gcj2P3K3PN5uVB5IFU-xUV84jil_CYyXLuA1v9kG2DH0Y1ypDZ7LUSSX0rGlH-XUX22hWFue0ENIsvoAqk5FPYXmfmIAWH6JpJ6_mHQdC9IP_gXOiXqXL2HLrFHWeKkXqb7by6UzoptT8Ro60EHzWd5VfmhAUv42UExriCZRW8YRW_LAFCFlauvRcwCL-lv8DsrbUgj1oPbxI_G9Z9q-8o_qqAqU9dwXlRgIboANc69AOX3JGtCKxfxlAEAVjrONzgO9LNyDHbUUv5oT7fv3HEgf6UQcbzbyQ8ISo1rq48ypIA9RWCb9hFmkjxUsIZJc0ScL3QHIdoXxxJ3Q0Zu7MgctSYMoCn77gsNr57HQJLsgXJDi53AgMsxdmHpj64kJTVoemRHpvbPv3REHAGXcAfixF0XQOEj-uDP9SDQ_ZXI-LaFbpdYyX6GWqQtpOnLjK4FIg6nKZD_W7MnLw6TWqh7o3BV-uouOmExvHgU2bbMpAtcDEVYL0rVh1jBIpSLk3UEZuidovj3TlnYVXG07hmJb-jsRzMplAK3UqPTaCa-3RrA8gklQTiampKoryI5G5dijCOTp0kYSWcYh50cqzG9pXqkH-73y3aeYa-RauCIJdFnq-17kfImdVCNSBELe_uW-dTDUUFZ22i8f5C_Ifn9xnGlqMUrq9AEJ2OOmuKgMJ0IuFhTqD18V4HhojWstF-O3B4_UyXte-ZN598PCFXkg_Uc8gsP5yQ3sFLMOWv-HDfuGnLDxDvT8T-yjUjRAem0IlaCsECR3eaAhSrP8yVe7OzFdRDYo-ZnZq_KW97U9aMTy7xUvoRP_fLGpVH0KLxjzEr9dTrQContTAu8knYWE0vcWRJZbPm28mzBVMwtXp8HMEi1-afUhJyhgkvRYRCaKUqyP3n3OAXN-5zggxUtH4tnvpy6Ew1xkBIQ9aFjWwJZRaE7-t1HhVjtXtyyQmPE6U-qax8IWxUqNE5cLi2J2Avjh5S69yfOOXwzWe_HDT7jFeCA8YLsx9G5iUFQpcvbnGlb4RWfTgLzpALlpssAT-sd33_zXXQ0oWvbsgOPibMmt5g7ggbNCy9J9onz2DE_CMjm6SySPS1l0eIpl3vB8dYgNt-e2Wowj7FrATYKLuVa_2VQY9juIOLyGd_VB1rrayJdliWzidzitZXbiqWbl6-XLLh28x7b3OheTRG_SWC3opXSiA2r4aTc3VvqAeMo2XtflWIuLjt7jvgtH_TwqR3oli-9bD1UeTT9AaWJRfS6xmMNlqbHw3fKXwTKGJEMQkyIkiXY4vOcMUYJBfaRkQbKRJ5wRGw3FLhgE56oSAhEPnjPZDx69-toWOziSJGWKR4p4iAaLNh2Xoeyi8Y9yjId7u07OwKNsOUqYo8GaF4hgA6kkrfofB1064vi2-_AUqGch4WFZQ5CyOyT3_FfKXxqR5HrHWXevT8zlXX4dFmiMt3rI5riojhVjMW9XwxUJFkvKW-n0SWPgx7phnT0Rew0WcY-pp86g04AjqxJ-WBXCYpR6z1_No6znnkPm3zaPKLDUgffx7ImRGfNxoIQGUQ5CV9P2v8W2mBaENDjDMuQh09wG0q7y0ttvj9PtqyRtYwB_VhxX5iiFl6d2No5IIJIpUK5QqykI06pWDU_7M4qoIKLwGgc8ICdUqqy3UgZZZxWnyuwuJP0FNgzjKxnZE8PBrtY-m9CSU_UYSuMtUw7cE1XsCvenDrGgj32OLjVoKkp8IrULovZxnSHhV8Cg0Q6mgtDmSuVGc4cqA_nvzK6JwGhAN1Zn2dY3xcdjt5HUqK9SxGMiQUM3yEtX5BsyBqyGmP8m8PBEtscoZpQ-qvDHEUpk7VIuimUhnDlzaFdSzMwMmEBMabHu5tBau5aU4eUmCxCWVrndeKlszgDqbuOgz6sJ5xGoVcmBR2ri80B_d_2ZkOZhq0_X9Dyisyb-ggYNLbUuoFYemVSqJ67pEnVILZRqUrlgWN-xQ6FTshQfLoAWFetUK-89bAAi8Ly76plHomz4NYGqWhfOz9ot09ivybHVY66BtxLYYGwcVeXLB2y3cqQ94Pyo7B-ixe6IQQOvWP0CO6hgkwNqYqThQaU8VEQQR2ehprcE0uE2t4bqLlsuNEZfrqGJx8UNxfoflVZXx2hqdmQN_r0-vQwjAXKvbElOkJZP0Yvs3Vf-Ym0UocU2a51_iQklxvpDqMVW4dmRCbvUcIcKmXX2C8xwqYg5z9SzKzyYqUZjvQZJ0xrF_T-uiQjKQAUxTxWI9K39xV-OL_KlTlR8OqSUrjzQS1mSloLLMFuQvctujaf0cH1a6yqGHUaUkHwTO0UvzIzR_-4LEmiGJSPQP0qDyqjr2RxqbRZRAgnjOMjjo9dZc4mf_2ywdeC2KAS56gtr8hfDI0lp7R6fIVLfrDAL2PzqIERjqoEyP3WAuqjsBiVw-tui3O1Te4tliQ7IhvLjeaG1LvuvbfI1z7A&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "\n\n \n \n DBM\n In-Stream Video\n \n \n\n\n\n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n IN\n 4\n 6260\n 0.0400USD\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 485459443-1\n Yes\n \n \n \n Silverpush-1687361105\n \n\n", + "adomain": [ + "disney.com" + ], + "cid": "sEJAtadBdhxs", + "crid": "485459443", + "w": 360, + "h": 727, + "mtype":2, + "ext": { + "origbidcpm": 0.04, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "video" + }, + "third_party_buyer_token": "AKAmf-CsVHgbcp3N0PM2mbsEk1oxApVbS1Hirp9rrNZ-_AFPCptW6_0kFrISwByjI8QMGTsTLgEB6Wq-fmTmi--xzsoyp4oME_pWeNmkLm0Na81xc7ZVv90IF3Pe1kZszpASOYUkxSXH" + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + { + "bid": { + "id": "iJQDNZRA", + "impid": "1", + "price": 0.04, + "burl": "https://localhost:8181/tracker/?p=iEUrKGOlRpwuy6LMCoEc8oew34TcCljlTEjAN1URYhXSxosy0IanKUQw36mX8Igm5v5887kijBb_kwdRT8OIJv1KQWLtEYJFkSJOdZJvSFNF_6Ir6_yaslz3Br4JuIUC9owtY46bQy0ltarrzOf8Xhq5XosFj88AVue5y5aBCxvdNF0nqtk1f_IN5h59yNBlUGTQ6_hAxOO_f27g3t1sjDpoojfZ5gcj2P3K3PN5uVB5IFU-xUV84jil_CYyXLuA1v9kG2DH0Y1ypDZ7LUSSX0rGlH-XUX22hWFue0ENIsvoAqk5FPYXmfmIAWH6JpJ6_mHQdC9IP_gXOiXqXL2HLrFHWeKkXqb7by6UzoptT8Ro60EHzWd5VfmhAUv42UExriCZRW8YRW_LAFCFlauvRcwCL-lv8DsrbUgj1oPbxI_G9Z9q-8o_qqAqU9dwXlRgIboANc69AOX3JGtCKxfxlAEAVjrONzgO9LNyDHbUUv5oT7fv3HEgf6UQcbzbyQ8ISo1rq48ypIA9RWCb9hFmkjxUsIZJc0ScL3QHIdoXxxJ3Q0Zu7MgctSYMoCn77gsNr57HQJLsgXJDi53AgMsxdmHpj64kJTVoemRHpvbPv3REHAGXcAfixF0XQOEj-uDP9SDQ_ZXI-LaFbpdYyX6GWqQtpOnLjK4FIg6nKZD_W7MnLw6TWqh7o3BV-uouOmExvHgU2bbMpAtcDEVYL0rVh1jBIpSLk3UEZuidovj3TlnYVXG07hmJb-jsRzMplAK3UqPTaCa-3RrA8gklQTiampKoryI5G5dijCOTp0kYSWcYh50cqzG9pXqkH-73y3aeYa-RauCIJdFnq-17kfImdVCNSBELe_uW-dTDUUFZ22i8f5C_Ifn9xnGlqMUrq9AEJ2OOmuKgMJ0IuFhTqD18V4HhojWstF-O3B4_UyXte-ZN598PCFXkg_Uc8gsP5yQ3sFLMOWv-HDfuGnLDxDvT8T-yjUjRAem0IlaCsECR3eaAhSrP8yVe7OzFdRDYo-ZnZq_KW97U9aMTy7xUvoRP_fLGpVH0KLxjzEr9dTrQContTAu8knYWE0vcWRJZbPm28mzBVMwtXp8HMEi1-afUhJyhgkvRYRCaKUqyP3n3OAXN-5zggxUtH4tnvpy6Ew1xkBIQ9aFjWwJZRaE7-t1HhVjtXtyyQmPE6U-qax8IWxUqNE5cLi2J2Avjh5S69yfOOXwzWe_HDT7jFeCA8YLsx9G5iUFQpcvbnGlb4RWfTgLzpALlpssAT-sd33_zXXQ0oWvbsgOPibMmt5g7ggbNCy9J9onz2DE_CMjm6SySPS1l0eIpl3vB8dYgNt-e2Wowj7FrATYKLuVa_2VQY9juIOLyGd_VB1rrayJdliWzidzitZXbiqWbl6-XLLh28x7b3OheTRG_SWC3opXSiA2r4aTc3VvqAeMo2XtflWIuLjt7jvgtH_TwqR3oli-9bD1UeTT9AaWJRfS6xmMNlqbHw3fKXwTKGJEMQkyIkiXY4vOcMUYJBfaRkQbKRJ5wRGw3FLhgE56oSAhEPnjPZDx69-toWOziSJGWKR4p4iAaLNh2Xoeyi8Y9yjId7u07OwKNsOUqYo8GaF4hgA6kkrfofB1064vi2-_AUqGch4WFZQ5CyOyT3_FfKXxqR5HrHWXevT8zlXX4dFmiMt3rI5riojhVjMW9XwxUJFkvKW-n0SWPgx7phnT0Rew0WcY-pp86g04AjqxJ-WBXCYpR6z1_No6znnkPm3zaPKLDUgffx7ImRGfNxoIQGUQ5CV9P2v8W2mBaENDjDMuQh09wG0q7y0ttvj9PtqyRtYwB_VhxX5iiFl6d2No5IIJIpUK5QqykI06pWDU_7M4qoIKLwGgc8ICdUqqy3UgZZZxWnyuwuJP0FNgzjKxnZE8PBrtY-m9CSU_UYSuMtUw7cE1XsCvenDrGgj32OLjVoKkp8IrULovZxnSHhV8Cg0Q6mgtDmSuVGc4cqA_nvzK6JwGhAN1Zn2dY3xcdjt5HUqK9SxGMiQUM3yEtX5BsyBqyGmP8m8PBEtscoZpQ-qvDHEUpk7VIuimUhnDlzaFdSzMwMmEBMabHu5tBau5aU4eUmCxCWVrndeKlszgDqbuOgz6sJ5xGoVcmBR2ri80B_d_2ZkOZhq0_X9Dyisyb-ggYNLbUuoFYemVSqJ67pEnVILZRqUrlgWN-xQ6FTshQfLoAWFetUK-89bAAi8Ly76plHomz4NYGqWhfOz9ot09ivybHVY66BtxLYYGwcVeXLB2y3cqQ94Pyo7B-ixe6IQQOvWP0CO6hgkwNqYqThQaU8VEQQR2ehprcE0uE2t4bqLlsuNEZfrqGJx8UNxfoflVZXx2hqdmQN_r0-vQwjAXKvbElOkJZP0Yvs3Vf-Ym0UocU2a51_iQklxvpDqMVW4dmRCbvUcIcKmXX2C8xwqYg5z9SzKzyYqUZjvQZJ0xrF_T-uiQjKQAUxTxWI9K39xV-OL_KlTlR8OqSUrjzQS1mSloLLMFuQvctujaf0cH1a6yqGHUaUkHwTO0UvzIzR_-4LEmiGJSPQP0qDyqjr2RxqbRZRAgnjOMjjo9dZc4mf_2ywdeC2KAS56gtr8hfDI0lp7R6fIVLfrDAL2PzqIERjqoEyP3WAuqjsBiVw-tui3O1Te4tliQ7IhvLjeaG1LvuvbfI1z7A&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "\n\n \n \n DBM\n In-Stream Video\n \n \n\n\n\n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n IN\n 4\n 6260\n 0.0400USD\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 485459443-1\n Yes\n \n \n \n Silverpush-1687361105\n \n\n", + "adomain": [ + "disney.com" + ], + "cid": "sEJAtadBdhxs", + "crid": "485459443", + "w": 360, + "h": 727, + "mtype":2, + "ext": { + "origbidcpm": 0.04, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "video" + }, + "third_party_buyer_token": "AKAmf-CsVHgbcp3N0PM2mbsEk1oxApVbS1Hirp9rrNZ-_AFPCptW6_0kFrISwByjI8QMGTsTLgEB6Wq-fmTmi--xzsoyp4oME_pWeNmkLm0Na81xc7ZVv90IF3Pe1kZszpASOYUkxSXH" + } + }, + "type":"video" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/video.json b/adapters/silverpush/silverpushtest/exemplary/video.json new file mode 100644 index 00000000000..2f0dccef05a --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/video.json @@ -0,0 +1,250 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + "mimes": [ + "video/mp4" + ], + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + "minduration": 1, + "maxduration": 0, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + "mimes": [ + "video/mp4" + ], + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + "minduration": 1, + "maxduration": 120, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "iJQDNZRA", + "impid": "1", + "price": 0.04, + "burl": "https://localhost:8181/tracker/?p=iEUrKGOlRpwuy6LMCoEc8oew34TcCljlTEjAN1URYhXSxosy0IanKUQw36mX8Igm5v5887kijBb_kwdRT8OIJv1KQWLtEYJFkSJOdZJvSFNF_6Ir6_yaslz3Br4JuIUC9owtY46bQy0ltarrzOf8Xhq5XosFj88AVue5y5aBCxvdNF0nqtk1f_IN5h59yNBlUGTQ6_hAxOO_f27g3t1sjDpoojfZ5gcj2P3K3PN5uVB5IFU-xUV84jil_CYyXLuA1v9kG2DH0Y1ypDZ7LUSSX0rGlH-XUX22hWFue0ENIsvoAqk5FPYXmfmIAWH6JpJ6_mHQdC9IP_gXOiXqXL2HLrFHWeKkXqb7by6UzoptT8Ro60EHzWd5VfmhAUv42UExriCZRW8YRW_LAFCFlauvRcwCL-lv8DsrbUgj1oPbxI_G9Z9q-8o_qqAqU9dwXlRgIboANc69AOX3JGtCKxfxlAEAVjrONzgO9LNyDHbUUv5oT7fv3HEgf6UQcbzbyQ8ISo1rq48ypIA9RWCb9hFmkjxUsIZJc0ScL3QHIdoXxxJ3Q0Zu7MgctSYMoCn77gsNr57HQJLsgXJDi53AgMsxdmHpj64kJTVoemRHpvbPv3REHAGXcAfixF0XQOEj-uDP9SDQ_ZXI-LaFbpdYyX6GWqQtpOnLjK4FIg6nKZD_W7MnLw6TWqh7o3BV-uouOmExvHgU2bbMpAtcDEVYL0rVh1jBIpSLk3UEZuidovj3TlnYVXG07hmJb-jsRzMplAK3UqPTaCa-3RrA8gklQTiampKoryI5G5dijCOTp0kYSWcYh50cqzG9pXqkH-73y3aeYa-RauCIJdFnq-17kfImdVCNSBELe_uW-dTDUUFZ22i8f5C_Ifn9xnGlqMUrq9AEJ2OOmuKgMJ0IuFhTqD18V4HhojWstF-O3B4_UyXte-ZN598PCFXkg_Uc8gsP5yQ3sFLMOWv-HDfuGnLDxDvT8T-yjUjRAem0IlaCsECR3eaAhSrP8yVe7OzFdRDYo-ZnZq_KW97U9aMTy7xUvoRP_fLGpVH0KLxjzEr9dTrQContTAu8knYWE0vcWRJZbPm28mzBVMwtXp8HMEi1-afUhJyhgkvRYRCaKUqyP3n3OAXN-5zggxUtH4tnvpy6Ew1xkBIQ9aFjWwJZRaE7-t1HhVjtXtyyQmPE6U-qax8IWxUqNE5cLi2J2Avjh5S69yfOOXwzWe_HDT7jFeCA8YLsx9G5iUFQpcvbnGlb4RWfTgLzpALlpssAT-sd33_zXXQ0oWvbsgOPibMmt5g7ggbNCy9J9onz2DE_CMjm6SySPS1l0eIpl3vB8dYgNt-e2Wowj7FrATYKLuVa_2VQY9juIOLyGd_VB1rrayJdliWzidzitZXbiqWbl6-XLLh28x7b3OheTRG_SWC3opXSiA2r4aTc3VvqAeMo2XtflWIuLjt7jvgtH_TwqR3oli-9bD1UeTT9AaWJRfS6xmMNlqbHw3fKXwTKGJEMQkyIkiXY4vOcMUYJBfaRkQbKRJ5wRGw3FLhgE56oSAhEPnjPZDx69-toWOziSJGWKR4p4iAaLNh2Xoeyi8Y9yjId7u07OwKNsOUqYo8GaF4hgA6kkrfofB1064vi2-_AUqGch4WFZQ5CyOyT3_FfKXxqR5HrHWXevT8zlXX4dFmiMt3rI5riojhVjMW9XwxUJFkvKW-n0SWPgx7phnT0Rew0WcY-pp86g04AjqxJ-WBXCYpR6z1_No6znnkPm3zaPKLDUgffx7ImRGfNxoIQGUQ5CV9P2v8W2mBaENDjDMuQh09wG0q7y0ttvj9PtqyRtYwB_VhxX5iiFl6d2No5IIJIpUK5QqykI06pWDU_7M4qoIKLwGgc8ICdUqqy3UgZZZxWnyuwuJP0FNgzjKxnZE8PBrtY-m9CSU_UYSuMtUw7cE1XsCvenDrGgj32OLjVoKkp8IrULovZxnSHhV8Cg0Q6mgtDmSuVGc4cqA_nvzK6JwGhAN1Zn2dY3xcdjt5HUqK9SxGMiQUM3yEtX5BsyBqyGmP8m8PBEtscoZpQ-qvDHEUpk7VIuimUhnDlzaFdSzMwMmEBMabHu5tBau5aU4eUmCxCWVrndeKlszgDqbuOgz6sJ5xGoVcmBR2ri80B_d_2ZkOZhq0_X9Dyisyb-ggYNLbUuoFYemVSqJ67pEnVILZRqUrlgWN-xQ6FTshQfLoAWFetUK-89bAAi8Ly76plHomz4NYGqWhfOz9ot09ivybHVY66BtxLYYGwcVeXLB2y3cqQ94Pyo7B-ixe6IQQOvWP0CO6hgkwNqYqThQaU8VEQQR2ehprcE0uE2t4bqLlsuNEZfrqGJx8UNxfoflVZXx2hqdmQN_r0-vQwjAXKvbElOkJZP0Yvs3Vf-Ym0UocU2a51_iQklxvpDqMVW4dmRCbvUcIcKmXX2C8xwqYg5z9SzKzyYqUZjvQZJ0xrF_T-uiQjKQAUxTxWI9K39xV-OL_KlTlR8OqSUrjzQS1mSloLLMFuQvctujaf0cH1a6yqGHUaUkHwTO0UvzIzR_-4LEmiGJSPQP0qDyqjr2RxqbRZRAgnjOMjjo9dZc4mf_2ywdeC2KAS56gtr8hfDI0lp7R6fIVLfrDAL2PzqIERjqoEyP3WAuqjsBiVw-tui3O1Te4tliQ7IhvLjeaG1LvuvbfI1z7A&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "\n\n \n \n DBM\n In-Stream Video\n \n \n\n\n\n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n IN\n 4\n 6260\n 0.0400USD\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 485459443-1\n Yes\n \n \n \n Silverpush-1687361105\n \n\n", + "adomain": [ + "disney.com" + ], + "cid": "sEJAtadBdhxs", + "crid": "485459443", + "w": 360, + "h": 727, + "mtype":2, + "ext": { + "origbidcpm": 0.04, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "video" + }, + "third_party_buyer_token": "AKAmf-CsVHgbcp3N0PM2mbsEk1oxApVbS1Hirp9rrNZ-_AFPCptW6_0kFrISwByjI8QMGTsTLgEB6Wq-fmTmi--xzsoyp4oME_pWeNmkLm0Na81xc7ZVv90IF3Pe1kZszpASOYUkxSXH" + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + { + "bid": { + "id": "iJQDNZRA", + "impid": "1", + "price": 0.04, + "burl": "https://localhost:8181/tracker/?p=iEUrKGOlRpwuy6LMCoEc8oew34TcCljlTEjAN1URYhXSxosy0IanKUQw36mX8Igm5v5887kijBb_kwdRT8OIJv1KQWLtEYJFkSJOdZJvSFNF_6Ir6_yaslz3Br4JuIUC9owtY46bQy0ltarrzOf8Xhq5XosFj88AVue5y5aBCxvdNF0nqtk1f_IN5h59yNBlUGTQ6_hAxOO_f27g3t1sjDpoojfZ5gcj2P3K3PN5uVB5IFU-xUV84jil_CYyXLuA1v9kG2DH0Y1ypDZ7LUSSX0rGlH-XUX22hWFue0ENIsvoAqk5FPYXmfmIAWH6JpJ6_mHQdC9IP_gXOiXqXL2HLrFHWeKkXqb7by6UzoptT8Ro60EHzWd5VfmhAUv42UExriCZRW8YRW_LAFCFlauvRcwCL-lv8DsrbUgj1oPbxI_G9Z9q-8o_qqAqU9dwXlRgIboANc69AOX3JGtCKxfxlAEAVjrONzgO9LNyDHbUUv5oT7fv3HEgf6UQcbzbyQ8ISo1rq48ypIA9RWCb9hFmkjxUsIZJc0ScL3QHIdoXxxJ3Q0Zu7MgctSYMoCn77gsNr57HQJLsgXJDi53AgMsxdmHpj64kJTVoemRHpvbPv3REHAGXcAfixF0XQOEj-uDP9SDQ_ZXI-LaFbpdYyX6GWqQtpOnLjK4FIg6nKZD_W7MnLw6TWqh7o3BV-uouOmExvHgU2bbMpAtcDEVYL0rVh1jBIpSLk3UEZuidovj3TlnYVXG07hmJb-jsRzMplAK3UqPTaCa-3RrA8gklQTiampKoryI5G5dijCOTp0kYSWcYh50cqzG9pXqkH-73y3aeYa-RauCIJdFnq-17kfImdVCNSBELe_uW-dTDUUFZ22i8f5C_Ifn9xnGlqMUrq9AEJ2OOmuKgMJ0IuFhTqD18V4HhojWstF-O3B4_UyXte-ZN598PCFXkg_Uc8gsP5yQ3sFLMOWv-HDfuGnLDxDvT8T-yjUjRAem0IlaCsECR3eaAhSrP8yVe7OzFdRDYo-ZnZq_KW97U9aMTy7xUvoRP_fLGpVH0KLxjzEr9dTrQContTAu8knYWE0vcWRJZbPm28mzBVMwtXp8HMEi1-afUhJyhgkvRYRCaKUqyP3n3OAXN-5zggxUtH4tnvpy6Ew1xkBIQ9aFjWwJZRaE7-t1HhVjtXtyyQmPE6U-qax8IWxUqNE5cLi2J2Avjh5S69yfOOXwzWe_HDT7jFeCA8YLsx9G5iUFQpcvbnGlb4RWfTgLzpALlpssAT-sd33_zXXQ0oWvbsgOPibMmt5g7ggbNCy9J9onz2DE_CMjm6SySPS1l0eIpl3vB8dYgNt-e2Wowj7FrATYKLuVa_2VQY9juIOLyGd_VB1rrayJdliWzidzitZXbiqWbl6-XLLh28x7b3OheTRG_SWC3opXSiA2r4aTc3VvqAeMo2XtflWIuLjt7jvgtH_TwqR3oli-9bD1UeTT9AaWJRfS6xmMNlqbHw3fKXwTKGJEMQkyIkiXY4vOcMUYJBfaRkQbKRJ5wRGw3FLhgE56oSAhEPnjPZDx69-toWOziSJGWKR4p4iAaLNh2Xoeyi8Y9yjId7u07OwKNsOUqYo8GaF4hgA6kkrfofB1064vi2-_AUqGch4WFZQ5CyOyT3_FfKXxqR5HrHWXevT8zlXX4dFmiMt3rI5riojhVjMW9XwxUJFkvKW-n0SWPgx7phnT0Rew0WcY-pp86g04AjqxJ-WBXCYpR6z1_No6znnkPm3zaPKLDUgffx7ImRGfNxoIQGUQ5CV9P2v8W2mBaENDjDMuQh09wG0q7y0ttvj9PtqyRtYwB_VhxX5iiFl6d2No5IIJIpUK5QqykI06pWDU_7M4qoIKLwGgc8ICdUqqy3UgZZZxWnyuwuJP0FNgzjKxnZE8PBrtY-m9CSU_UYSuMtUw7cE1XsCvenDrGgj32OLjVoKkp8IrULovZxnSHhV8Cg0Q6mgtDmSuVGc4cqA_nvzK6JwGhAN1Zn2dY3xcdjt5HUqK9SxGMiQUM3yEtX5BsyBqyGmP8m8PBEtscoZpQ-qvDHEUpk7VIuimUhnDlzaFdSzMwMmEBMabHu5tBau5aU4eUmCxCWVrndeKlszgDqbuOgz6sJ5xGoVcmBR2ri80B_d_2ZkOZhq0_X9Dyisyb-ggYNLbUuoFYemVSqJ67pEnVILZRqUrlgWN-xQ6FTshQfLoAWFetUK-89bAAi8Ly76plHomz4NYGqWhfOz9ot09ivybHVY66BtxLYYGwcVeXLB2y3cqQ94Pyo7B-ixe6IQQOvWP0CO6hgkwNqYqThQaU8VEQQR2ehprcE0uE2t4bqLlsuNEZfrqGJx8UNxfoflVZXx2hqdmQN_r0-vQwjAXKvbElOkJZP0Yvs3Vf-Ym0UocU2a51_iQklxvpDqMVW4dmRCbvUcIcKmXX2C8xwqYg5z9SzKzyYqUZjvQZJ0xrF_T-uiQjKQAUxTxWI9K39xV-OL_KlTlR8OqSUrjzQS1mSloLLMFuQvctujaf0cH1a6yqGHUaUkHwTO0UvzIzR_-4LEmiGJSPQP0qDyqjr2RxqbRZRAgnjOMjjo9dZc4mf_2ywdeC2KAS56gtr8hfDI0lp7R6fIVLfrDAL2PzqIERjqoEyP3WAuqjsBiVw-tui3O1Te4tliQ7IhvLjeaG1LvuvbfI1z7A&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "\n\n \n \n DBM\n In-Stream Video\n \n \n\n\n\n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n IN\n 4\n 6260\n 0.0400USD\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 485459443-1\n Yes\n \n \n \n Silverpush-1687361105\n \n\n", + "adomain": [ + "disney.com" + ], + "cid": "sEJAtadBdhxs", + "crid": "485459443", + "w": 360, + "h": 727, + "mtype":2, + "ext": { + "origbidcpm": 0.04, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "video" + }, + "third_party_buyer_token": "AKAmf-CsVHgbcp3N0PM2mbsEk1oxApVbS1Hirp9rrNZ-_AFPCptW6_0kFrISwByjI8QMGTsTLgEB6Wq-fmTmi--xzsoyp4oME_pWeNmkLm0Na81xc7ZVv90IF3Pe1kZszpASOYUkxSXH" + } + }, + "type":"video" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/exemplary/video_min_max_duration.json b/adapters/silverpush/silverpushtest/exemplary/video_min_max_duration.json new file mode 100644 index 00000000000..d0ac6889362 --- /dev/null +++ b/adapters/silverpush/silverpushtest/exemplary/video_min_max_duration.json @@ -0,0 +1,250 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + "mimes": [ + "video/mp4" + ], + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + "minduration": 10, + "maxduration": 5, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + "mimes": [ + "video/mp4" + ], + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + + "maxduration": 10, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "iJQDNZRA", + "impid": "1", + "price": 0.04, + "burl": "https://localhost:8181/tracker/?p=iEUrKGOlRpwuy6LMCoEc8oew34TcCljlTEjAN1URYhXSxosy0IanKUQw36mX8Igm5v5887kijBb_kwdRT8OIJv1KQWLtEYJFkSJOdZJvSFNF_6Ir6_yaslz3Br4JuIUC9owtY46bQy0ltarrzOf8Xhq5XosFj88AVue5y5aBCxvdNF0nqtk1f_IN5h59yNBlUGTQ6_hAxOO_f27g3t1sjDpoojfZ5gcj2P3K3PN5uVB5IFU-xUV84jil_CYyXLuA1v9kG2DH0Y1ypDZ7LUSSX0rGlH-XUX22hWFue0ENIsvoAqk5FPYXmfmIAWH6JpJ6_mHQdC9IP_gXOiXqXL2HLrFHWeKkXqb7by6UzoptT8Ro60EHzWd5VfmhAUv42UExriCZRW8YRW_LAFCFlauvRcwCL-lv8DsrbUgj1oPbxI_G9Z9q-8o_qqAqU9dwXlRgIboANc69AOX3JGtCKxfxlAEAVjrONzgO9LNyDHbUUv5oT7fv3HEgf6UQcbzbyQ8ISo1rq48ypIA9RWCb9hFmkjxUsIZJc0ScL3QHIdoXxxJ3Q0Zu7MgctSYMoCn77gsNr57HQJLsgXJDi53AgMsxdmHpj64kJTVoemRHpvbPv3REHAGXcAfixF0XQOEj-uDP9SDQ_ZXI-LaFbpdYyX6GWqQtpOnLjK4FIg6nKZD_W7MnLw6TWqh7o3BV-uouOmExvHgU2bbMpAtcDEVYL0rVh1jBIpSLk3UEZuidovj3TlnYVXG07hmJb-jsRzMplAK3UqPTaCa-3RrA8gklQTiampKoryI5G5dijCOTp0kYSWcYh50cqzG9pXqkH-73y3aeYa-RauCIJdFnq-17kfImdVCNSBELe_uW-dTDUUFZ22i8f5C_Ifn9xnGlqMUrq9AEJ2OOmuKgMJ0IuFhTqD18V4HhojWstF-O3B4_UyXte-ZN598PCFXkg_Uc8gsP5yQ3sFLMOWv-HDfuGnLDxDvT8T-yjUjRAem0IlaCsECR3eaAhSrP8yVe7OzFdRDYo-ZnZq_KW97U9aMTy7xUvoRP_fLGpVH0KLxjzEr9dTrQContTAu8knYWE0vcWRJZbPm28mzBVMwtXp8HMEi1-afUhJyhgkvRYRCaKUqyP3n3OAXN-5zggxUtH4tnvpy6Ew1xkBIQ9aFjWwJZRaE7-t1HhVjtXtyyQmPE6U-qax8IWxUqNE5cLi2J2Avjh5S69yfOOXwzWe_HDT7jFeCA8YLsx9G5iUFQpcvbnGlb4RWfTgLzpALlpssAT-sd33_zXXQ0oWvbsgOPibMmt5g7ggbNCy9J9onz2DE_CMjm6SySPS1l0eIpl3vB8dYgNt-e2Wowj7FrATYKLuVa_2VQY9juIOLyGd_VB1rrayJdliWzidzitZXbiqWbl6-XLLh28x7b3OheTRG_SWC3opXSiA2r4aTc3VvqAeMo2XtflWIuLjt7jvgtH_TwqR3oli-9bD1UeTT9AaWJRfS6xmMNlqbHw3fKXwTKGJEMQkyIkiXY4vOcMUYJBfaRkQbKRJ5wRGw3FLhgE56oSAhEPnjPZDx69-toWOziSJGWKR4p4iAaLNh2Xoeyi8Y9yjId7u07OwKNsOUqYo8GaF4hgA6kkrfofB1064vi2-_AUqGch4WFZQ5CyOyT3_FfKXxqR5HrHWXevT8zlXX4dFmiMt3rI5riojhVjMW9XwxUJFkvKW-n0SWPgx7phnT0Rew0WcY-pp86g04AjqxJ-WBXCYpR6z1_No6znnkPm3zaPKLDUgffx7ImRGfNxoIQGUQ5CV9P2v8W2mBaENDjDMuQh09wG0q7y0ttvj9PtqyRtYwB_VhxX5iiFl6d2No5IIJIpUK5QqykI06pWDU_7M4qoIKLwGgc8ICdUqqy3UgZZZxWnyuwuJP0FNgzjKxnZE8PBrtY-m9CSU_UYSuMtUw7cE1XsCvenDrGgj32OLjVoKkp8IrULovZxnSHhV8Cg0Q6mgtDmSuVGc4cqA_nvzK6JwGhAN1Zn2dY3xcdjt5HUqK9SxGMiQUM3yEtX5BsyBqyGmP8m8PBEtscoZpQ-qvDHEUpk7VIuimUhnDlzaFdSzMwMmEBMabHu5tBau5aU4eUmCxCWVrndeKlszgDqbuOgz6sJ5xGoVcmBR2ri80B_d_2ZkOZhq0_X9Dyisyb-ggYNLbUuoFYemVSqJ67pEnVILZRqUrlgWN-xQ6FTshQfLoAWFetUK-89bAAi8Ly76plHomz4NYGqWhfOz9ot09ivybHVY66BtxLYYGwcVeXLB2y3cqQ94Pyo7B-ixe6IQQOvWP0CO6hgkwNqYqThQaU8VEQQR2ehprcE0uE2t4bqLlsuNEZfrqGJx8UNxfoflVZXx2hqdmQN_r0-vQwjAXKvbElOkJZP0Yvs3Vf-Ym0UocU2a51_iQklxvpDqMVW4dmRCbvUcIcKmXX2C8xwqYg5z9SzKzyYqUZjvQZJ0xrF_T-uiQjKQAUxTxWI9K39xV-OL_KlTlR8OqSUrjzQS1mSloLLMFuQvctujaf0cH1a6yqGHUaUkHwTO0UvzIzR_-4LEmiGJSPQP0qDyqjr2RxqbRZRAgnjOMjjo9dZc4mf_2ywdeC2KAS56gtr8hfDI0lp7R6fIVLfrDAL2PzqIERjqoEyP3WAuqjsBiVw-tui3O1Te4tliQ7IhvLjeaG1LvuvbfI1z7A&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "\n\n \n \n DBM\n In-Stream Video\n \n \n\n\n\n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n IN\n 4\n 6260\n 0.0400USD\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 485459443-1\n Yes\n \n \n \n Silverpush-1687361105\n \n\n", + "adomain": [ + "disney.com" + ], + "cid": "sEJAtadBdhxs", + "crid": "485459443", + "w": 360, + "h": 727, + "mtype":2, + "ext": { + "origbidcpm": 0.04, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "video" + }, + "third_party_buyer_token": "AKAmf-CsVHgbcp3N0PM2mbsEk1oxApVbS1Hirp9rrNZ-_AFPCptW6_0kFrISwByjI8QMGTsTLgEB6Wq-fmTmi--xzsoyp4oME_pWeNmkLm0Na81xc7ZVv90IF3Pe1kZszpASOYUkxSXH" + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + { + "bid": { + "id": "iJQDNZRA", + "impid": "1", + "price": 0.04, + "burl": "https://localhost:8181/tracker/?p=iEUrKGOlRpwuy6LMCoEc8oew34TcCljlTEjAN1URYhXSxosy0IanKUQw36mX8Igm5v5887kijBb_kwdRT8OIJv1KQWLtEYJFkSJOdZJvSFNF_6Ir6_yaslz3Br4JuIUC9owtY46bQy0ltarrzOf8Xhq5XosFj88AVue5y5aBCxvdNF0nqtk1f_IN5h59yNBlUGTQ6_hAxOO_f27g3t1sjDpoojfZ5gcj2P3K3PN5uVB5IFU-xUV84jil_CYyXLuA1v9kG2DH0Y1ypDZ7LUSSX0rGlH-XUX22hWFue0ENIsvoAqk5FPYXmfmIAWH6JpJ6_mHQdC9IP_gXOiXqXL2HLrFHWeKkXqb7by6UzoptT8Ro60EHzWd5VfmhAUv42UExriCZRW8YRW_LAFCFlauvRcwCL-lv8DsrbUgj1oPbxI_G9Z9q-8o_qqAqU9dwXlRgIboANc69AOX3JGtCKxfxlAEAVjrONzgO9LNyDHbUUv5oT7fv3HEgf6UQcbzbyQ8ISo1rq48ypIA9RWCb9hFmkjxUsIZJc0ScL3QHIdoXxxJ3Q0Zu7MgctSYMoCn77gsNr57HQJLsgXJDi53AgMsxdmHpj64kJTVoemRHpvbPv3REHAGXcAfixF0XQOEj-uDP9SDQ_ZXI-LaFbpdYyX6GWqQtpOnLjK4FIg6nKZD_W7MnLw6TWqh7o3BV-uouOmExvHgU2bbMpAtcDEVYL0rVh1jBIpSLk3UEZuidovj3TlnYVXG07hmJb-jsRzMplAK3UqPTaCa-3RrA8gklQTiampKoryI5G5dijCOTp0kYSWcYh50cqzG9pXqkH-73y3aeYa-RauCIJdFnq-17kfImdVCNSBELe_uW-dTDUUFZ22i8f5C_Ifn9xnGlqMUrq9AEJ2OOmuKgMJ0IuFhTqD18V4HhojWstF-O3B4_UyXte-ZN598PCFXkg_Uc8gsP5yQ3sFLMOWv-HDfuGnLDxDvT8T-yjUjRAem0IlaCsECR3eaAhSrP8yVe7OzFdRDYo-ZnZq_KW97U9aMTy7xUvoRP_fLGpVH0KLxjzEr9dTrQContTAu8knYWE0vcWRJZbPm28mzBVMwtXp8HMEi1-afUhJyhgkvRYRCaKUqyP3n3OAXN-5zggxUtH4tnvpy6Ew1xkBIQ9aFjWwJZRaE7-t1HhVjtXtyyQmPE6U-qax8IWxUqNE5cLi2J2Avjh5S69yfOOXwzWe_HDT7jFeCA8YLsx9G5iUFQpcvbnGlb4RWfTgLzpALlpssAT-sd33_zXXQ0oWvbsgOPibMmt5g7ggbNCy9J9onz2DE_CMjm6SySPS1l0eIpl3vB8dYgNt-e2Wowj7FrATYKLuVa_2VQY9juIOLyGd_VB1rrayJdliWzidzitZXbiqWbl6-XLLh28x7b3OheTRG_SWC3opXSiA2r4aTc3VvqAeMo2XtflWIuLjt7jvgtH_TwqR3oli-9bD1UeTT9AaWJRfS6xmMNlqbHw3fKXwTKGJEMQkyIkiXY4vOcMUYJBfaRkQbKRJ5wRGw3FLhgE56oSAhEPnjPZDx69-toWOziSJGWKR4p4iAaLNh2Xoeyi8Y9yjId7u07OwKNsOUqYo8GaF4hgA6kkrfofB1064vi2-_AUqGch4WFZQ5CyOyT3_FfKXxqR5HrHWXevT8zlXX4dFmiMt3rI5riojhVjMW9XwxUJFkvKW-n0SWPgx7phnT0Rew0WcY-pp86g04AjqxJ-WBXCYpR6z1_No6znnkPm3zaPKLDUgffx7ImRGfNxoIQGUQ5CV9P2v8W2mBaENDjDMuQh09wG0q7y0ttvj9PtqyRtYwB_VhxX5iiFl6d2No5IIJIpUK5QqykI06pWDU_7M4qoIKLwGgc8ICdUqqy3UgZZZxWnyuwuJP0FNgzjKxnZE8PBrtY-m9CSU_UYSuMtUw7cE1XsCvenDrGgj32OLjVoKkp8IrULovZxnSHhV8Cg0Q6mgtDmSuVGc4cqA_nvzK6JwGhAN1Zn2dY3xcdjt5HUqK9SxGMiQUM3yEtX5BsyBqyGmP8m8PBEtscoZpQ-qvDHEUpk7VIuimUhnDlzaFdSzMwMmEBMabHu5tBau5aU4eUmCxCWVrndeKlszgDqbuOgz6sJ5xGoVcmBR2ri80B_d_2ZkOZhq0_X9Dyisyb-ggYNLbUuoFYemVSqJ67pEnVILZRqUrlgWN-xQ6FTshQfLoAWFetUK-89bAAi8Ly76plHomz4NYGqWhfOz9ot09ivybHVY66BtxLYYGwcVeXLB2y3cqQ94Pyo7B-ixe6IQQOvWP0CO6hgkwNqYqThQaU8VEQQR2ehprcE0uE2t4bqLlsuNEZfrqGJx8UNxfoflVZXx2hqdmQN_r0-vQwjAXKvbElOkJZP0Yvs3Vf-Ym0UocU2a51_iQklxvpDqMVW4dmRCbvUcIcKmXX2C8xwqYg5z9SzKzyYqUZjvQZJ0xrF_T-uiQjKQAUxTxWI9K39xV-OL_KlTlR8OqSUrjzQS1mSloLLMFuQvctujaf0cH1a6yqGHUaUkHwTO0UvzIzR_-4LEmiGJSPQP0qDyqjr2RxqbRZRAgnjOMjjo9dZc4mf_2ywdeC2KAS56gtr8hfDI0lp7R6fIVLfrDAL2PzqIERjqoEyP3WAuqjsBiVw-tui3O1Te4tliQ7IhvLjeaG1LvuvbfI1z7A&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "\n\n \n \n DBM\n In-Stream Video\n \n \n\n\n\n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n IN\n 4\n 6260\n 0.0400USD\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 485459443-1\n Yes\n \n \n \n Silverpush-1687361105\n \n\n", + "adomain": [ + "disney.com" + ], + "cid": "sEJAtadBdhxs", + "crid": "485459443", + "w": 360, + "h": 727, + "mtype":2, + "ext": { + "origbidcpm": 0.04, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "video" + }, + "third_party_buyer_token": "AKAmf-CsVHgbcp3N0PM2mbsEk1oxApVbS1Hirp9rrNZ-_AFPCptW6_0kFrISwByjI8QMGTsTLgEB6Wq-fmTmi--xzsoyp4oME_pWeNmkLm0Na81xc7ZVv90IF3Pe1kZszpASOYUkxSXH" + } + }, + "type":"video" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-banner-format.json b/adapters/silverpush/silverpushtest/supplemental/bad-banner-format.json new file mode 100644 index 00000000000..78a7d09dbc9 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-banner-format.json @@ -0,0 +1,94 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }] + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No sizes provided for Banner.", + "comparison": "literal" + } + ] + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-eids-data.json b/adapters/silverpush/silverpushtest/supplemental/bad-eids-data.json new file mode 100644 index 00000000000..d813bdf0bb0 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-eids-data.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": {"data":9} + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid user.ext.data.", + "comparison": "literal" + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-eids-value.json b/adapters/silverpush/silverpushtest/supplemental/bad-eids-value.json new file mode 100644 index 00000000000..f91b0fc7cce --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-eids-value.json @@ -0,0 +1,278 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "" + } + ] + }] + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 290, + "h": 260, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "" + } + ] + }] + } + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "seatbid": [ + { + "bid": [ + { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + } + ], + "seat": "silverpush" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "bids": [ + {"bid": { + "id": "TbthOMFy", + "impid": "1", + "price": 0.03, + "burl": "https://localhost:8181/tracker/?p=FAlHYmO6o9Zl4J-A9I0lpSo19KeQaWQU_nIsE-kC1tevBA0xn9BSCf1FxK6f-zkPdD88g5V1wAwJnsj3HFHsQMoWLEv7j-DOqwSR_qtcTs77QhThKsEptZTR1BfmIaiBzNK3PCqdbUzSAdcUv8ftAZkzKBpRcoZuFYUFiSCRq014TN8AOhmgartsPmNuex_hS9Zq4Px-SsZkbGzL9MoBNfygT8v-eIC7lgDdKIlhUfcTOwHqcVx2Ji3vHQieaBDBlw2GTAATu1KEwLfS2hqzh0XQEI-TFBjvrvEypqfjQMdTtUcS1N34Pc7WPPgVbJk4X2p8DeoTBpKpCQJHRvveYUvC46WySfNha1lv_MB0Mf1hadYrDDqt3hjZa0o-7wRZsjGutVf2bM-I4LUSSNMCTNJF48ooJ_cZMd6Z0GWWQGZpM5yG5qUBJKtvN4ZbHTNftqyspsy_XyMD9c2Bl4QZA3Hn-McV0mQ_eelyCg-2vtLCDEOq4BsqhPNHy5jUsIC-kJYSng1LM_sAi6SAVhLWj0cH7hTGqPNhvTevCfd6o-B1v4ynptVnDmmBT7Mz4pDNWSYiJapg9QnBwjmENG1hbTtrxQoSVCXhhWgjsoQOB6XA_RdHmGqqQ4MgSn0pjKi--jnKCloXykuh3CM8a8PGzbNRk8aCHucTA-g12bmM5xsZg08hTW44eB-CJ7DHltjSc8FuDG2866YSXWahG488QWYmS-_AEBMSV2mIk49BWOWSOfjDK0Wo0_k9FqaoPpB-rtdsUMo-0VWLlZInDNJ1-BZgGC0CKx3BgYTonAoKxU5w86OayNr_kOGHzFKMMt2a7d6lTZmPz-x8ndFIYzsrzLMLxvvwLs2FZnHhobmPHgwDCO94QhrHtHC31edqa2T3QnlBnEg2fovlzi44a2vItNIZYYdhxlGKvfW4BwgPVc0BQVxl9voP-p4cytPrstWODIZ_2D6HiU1vdlZyXBFoPaeovOHE5Nvf1n9IQOlDRmp-7tE3Nx6iS_jiqnGZYIFKSwOGL7gUyoeYKZHc7tN_XUg6cxzBTf1ERC7C5StsLs7VCXCqWZg0t-RSeCdQ4zLt2eZEB_60KpW9RtXo5aALR49OsBLrXe2gldLKMwm9Waa6LZC1R5hj8T4gnpvjEC9jLyFEt7TmVRDkXWeXYiKW2cZCpZjPxv8S5cb0FPliR4tT3lkkOoRhlP9yWiVoHp58CGcfNonBvvwG9rMUdAqqVJD7Agw-v6jpLl_v3nk5ujdD09YT8Da6peM5m5J3Qmm7eTyUsm8zNroHeDSkcUh6kkpvUbrpvaFNaefIw1e1kzR7ONG9SWmpULSIygX4Ibh6FXRbyXw3Ekx-UQLS2EZ2b-MCuL08lyjGioRnDk8zB5jZo-0DKql93CbtDnjJbV-Q6a3Zy0L3zpyCx4V6mnd5kpe7USU-am-dy0aSAgNWkERkcc-v2l4oNVn9gLG5C5L7CS_xA1gceds34LfKFKFn0PwCZglpDUoOk1PZUWY6dQbTnC3qpK4_quEZxU4gZxoMlzsD1DiyX-RtXfqc-vUeKCVUzfz4IaL4BWizOh5c_YNdUib6d70DXGm_eKIBMVrSFGKuXelo2g6QsfosDV-qxnqhkwaFiAVzGFfrRYj5DYVHJHsr4TJ-7_c-OMX5ncr_3FiFHUk3ZVHVOUxTW3jfgWnnHSoPDmy9M2FAcbFQnbgnp9TB_Zmw7xxZXrzXvZuAbFbDqN34hIhqdz-U8I4KfituMY1HCCpgz7wCh6etG6868-g5wu6BRR7qv4F5ID4TEZzQOKodx-Hy5hJcBPwgphMMt6Ch7q_v4Hur-e-JMoJhjzsatxSc5klv-ZrJF6GkfMchMPwHYtfIyBcEKCQHKZAMsaJN20EhRcmNjMPS1thFqN4TXQSPVD__j4yn_KF175Xz7VAnIzfoFW9NiE9_trtZGPiJ&wn_wp=${AUCTION_PRICE}&wn_aid=${AUCTION_ID}&wn_abi=${AUCTION_BID_ID}&wn_aii=${AUCTION_IMP_ID}&wn_aai=${AUCTION_AD_ID}&wn_asi=${AUCTION_SEAT_ID}", + "adm": "https://adrta.com/i?clid=slp&paid=choc&avid=2008&caid=&plid=cr39487593745jefoijdf&publisherId=17281&siteId=&lineItemId=QKWUYk&kv1=360x640&kv2=&kv3=Uyb8g9qJmfcma4ABC&kv7=159&kv10=Optus&kv4=110.33.122.75&kv11=7360132c-cc30-4334-b3fe-c3ec98e27a0e&kv12=3&kv15=AUS&kv26=Android&kv16=-35.342200&kv17=149.210100&kv18=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv19=9e13d2bf-e968-4065-b4db-852123b627a9&kv25=a.f.fnf.mod.returns.rapper.music.funkin.fire&kv27=Mozilla%2F5.0+%28Linux%3B+Android+11%3B+CPH2209+Build%2FRP1A.200720.011%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.0+Chrome%2F100.0.4896.127+Mobile+Safari%2F537.36&kv24=OTT/CTV", + "adomain": [ + "linio.com.co" + ], + "cid": "FmQOmKiRUsLh", + "crid": "cr39487593745jefoijdf", + "w": 360, + "h": 640, + "mtype":1, + "ext": { + "origbidcpm": 0.03, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "silverpush" + }, + "type": "banner" + } + } + }, + "type":"banner" + } + ], + "cur": "USD" + + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-imp-ext-bidder.json b/adapters/silverpush/silverpushtest/supplemental/bad-imp-ext-bidder.json new file mode 100644 index 00000000000..5bedcfe34c6 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-imp-ext-bidder.json @@ -0,0 +1,86 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder":98 + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go value of type openrtb_ext.ImpExtSilverpush", + "comparison": "literal" + } + ] + + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-imp-ext.json b/adapters/silverpush/silverpushtest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..7c5b7af90e2 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-imp-ext.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": 98 + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] + + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-publisherId.json b/adapters/silverpush/silverpushtest/supplemental/bad-publisherId.json new file mode 100644 index 00000000000..97f292d670c --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-publisherId.json @@ -0,0 +1,82 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Missing publisherId parameter.", + "comparison": "literal" + } + ] + + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-response-unmarshal.json b/adapters/silverpush/silverpushtest/supplemental/bad-response-unmarshal.json new file mode 100644 index 00000000000..c48c48f1bf0 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-response-unmarshal.json @@ -0,0 +1,184 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 2, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "macOS", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 200, + "body": "for unmarshal error" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-user-ext-eids.json b/adapters/silverpush/silverpushtest/supplemental/bad-user-ext-eids.json new file mode 100644 index 00000000000..11922cd54b2 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-user-ext-eids.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC", + "ext": 99 + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid user.ext.", + "comparison": "literal" + } + ] + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/bad-video-format.json b/adapters/silverpush/silverpushtest/supplemental/bad-video-format.json new file mode 100644 index 00000000000..62d330679f2 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/bad-video-format.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "video": { + "api": [ + 1, + 2, + 4, + 6 + ], + + "protocols": [ + 4, + 5, + 6, + 7 + ], + "linearity": 1, + "placement": 1, + "minduration": 0, + "maxduration": 60, + "startdelay": 0, + "w": 320, + "h": 250 + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 3, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Invalid or missing video field(s)", + "comparison": "literal" + } + ] + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/status-204-resp.json b/adapters/silverpush/silverpushtest/supplemental/status-204-resp.json new file mode 100644 index 00000000000..fa95a6af1c7 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/status-204-resp.json @@ -0,0 +1,188 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 204, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + + } + } + } + ] + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/status-400-resp.json b/adapters/silverpush/silverpushtest/supplemental/status-400-resp.json new file mode 100644 index 00000000000..1a3c57f6eb4 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/status-400-resp.json @@ -0,0 +1,194 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 400, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] + + } \ No newline at end of file diff --git a/adapters/silverpush/silverpushtest/supplemental/status-500-resp.json b/adapters/silverpush/silverpushtest/supplemental/status-500-resp.json new file mode 100644 index 00000000000..86dff666431 --- /dev/null +++ b/adapters/silverpush/silverpushtest/supplemental/status-500-resp.json @@ -0,0 +1,194 @@ +{ + "mockBidRequest":{ + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "http://localhost:8080/bidder/?identifier=5krH8Q", + "body": { + + "id": "X4lwEgAAyXAKlqvGCwa9kg", + "imp": [ + { + "id": "1", + "banner": { + "w": 300, + "h": 50, + "pos": 3, + "mimes": [ + "image/jpeg", + "image/gif" + ], + "expdir": [ + 1, + 2, + 3, + 4 + ], + + "format": [ + { + "w": 290, + "h": 260 + } + ] + }, + "tagid": "2612792908", + "bidfloor": 0.05, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "publisherId": "publisher123", + "bidfloor": 0.05 + } + } + } + ], + "app": { + "publisher": { + "id": "publisher123" + } + }, + "device": { + "devicetype": 1, + "dnt": 0, + "h": 2340, + "ifa": "9e13d2bf-e968-4065-b4db-852123b627a9", + "ip": "110.33.122.75", + "js": 1, + "make": "OPPO", + "model": "CPH2209", + "os": "Android", + "osv": "11", + "pxratio": 3, + "ua": "Mozilla/5.0 (Linux; Android 11; CPH2209 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36", + "w": 1080 + }, + "site": { + "domain": "stg.betterbutter.in", + "publisher": { + "id": "publisher123" + } + }, + "user": { + "id": "Uyb8g9qJmfcma4ABC" + }, + "at": 1, + "tmax": 1300, + "cur": [ + "USD" + ], + "ext": { + "bc": "sp_pb_ortb_1.0.0", + "publisherId": "publisher123" + } + } + }, + "mockResponse": { + "status": 500, + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "body": { + + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] + + } \ No newline at end of file diff --git a/adapters/smaato/smaato.go b/adapters/smaato/smaato.go index debf4669f39..e0892ef50fb 100644 --- a/adapters/smaato/smaato.go +++ b/adapters/smaato/smaato.go @@ -17,7 +17,7 @@ import ( "github.com/prebid/prebid-server/util/timeutil" ) -const clientVersion = "prebid_server_0.5" +const clientVersion = "prebid_server_0.6" type adMarkupType string @@ -464,6 +464,11 @@ func setImpForAdspace(imp *openrtb2.Imp) error { return &errortypes.BadInput{Message: "Missing adspaceId parameter."} } + impExt, err := makeImpExt(&imp.Ext) + if err != nil { + return err + } + if imp.Banner != nil { bannerCopy, err := setBannerDimension(imp.Banner) if err != nil { @@ -471,13 +476,13 @@ func setImpForAdspace(imp *openrtb2.Imp) error { } imp.Banner = bannerCopy imp.TagID = adSpaceID - imp.Ext = nil + imp.Ext = impExt return nil } if imp.Video != nil || imp.Native != nil { imp.TagID = adSpaceID - imp.Ext = nil + imp.Ext = impExt return nil } @@ -494,6 +499,11 @@ func setImpForAdBreak(imps []openrtb2.Imp) error { return &errortypes.BadInput{Message: "Missing adbreakId parameter."} } + impExt, err := makeImpExt(&imps[0].Ext) + if err != nil { + return err + } + for i := range imps { imps[i].TagID = adBreakID imps[i].Ext = nil @@ -506,9 +516,33 @@ func setImpForAdBreak(imps []openrtb2.Imp) error { imps[i].Video = &videoCopy } + imps[0].Ext = impExt + return nil } +func makeImpExt(impExtRaw *json.RawMessage) (json.RawMessage, error) { + var impExt openrtb_ext.ExtImpExtraDataSmaato + + if err := json.Unmarshal(*impExtRaw, &impExt); err != nil { + return nil, &errortypes.BadInput{Message: "Invalid imp.ext."} + } + + if impExtSkadnRaw := impExt.Skadn; impExtSkadnRaw != nil { + var impExtSkadn map[string]json.RawMessage + + if err := json.Unmarshal(impExtSkadnRaw, &impExtSkadn); err != nil { + return nil, &errortypes.BadInput{Message: "Invalid imp.ext.skadn."} + } + } + + if impExtJson, err := json.Marshal(impExt); string(impExtJson) != "{}" { + return impExtJson, err + } else { + return nil, nil + } +} + func setBannerDimension(banner *openrtb2.Banner) (*openrtb2.Banner, error) { if banner.W != nil && banner.H != nil { return banner, nil diff --git a/adapters/smaato/smaatotest/exemplary/multiple-impressions-skadn.json b/adapters/smaato/smaatotest/exemplary/multiple-impressions-skadn.json new file mode 100644 index 00000000000..cbc1f1e3440 --- /dev/null +++ b/adapters/smaato/smaatotest/exemplary/multiple-impressions-skadn.json @@ -0,0 +1,510 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "ext": { + "data": { + "keywords": "power tools", + "search": "drill", + "content": { + "userrating": 4 + } + } + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "bidfloor": 0.00123, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "postbid_iframe", + "video": { + "mimes": [ + "video/mp4", + "video/quicktime", + "video/3gpp", + "video/x-m4v" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ], + "ext": { + "rewarded": 0 + } + }, + "bidfloor": 0.00456, + "ext": { + "bidder": { + "publisherId": "1100042526", + "adspaceId": "130563104" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047118", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString", + "data": { + "keywords": "a,b", + "gender": "M", + "yob": 1984, + "geo": { + "country": "ca" + } + } + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "bidfloor": 0.00123, + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "postbid_iframe", + "tagid": "130563104", + "bidfloor": 0.00456, + "video": { + "w": 1024, + "h": 768, + "ext": { + "rewarded": 0 + }, + "mimes": [ + "video/mp4", + "video/quicktime", + "video/3gpp", + "video/x-m4v" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ] + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047118", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042526" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6524", + "crid": "CR69382", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "postbid_iframe", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047118", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "
\"\"\"\"
", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50, + "exp": 300, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6524", + "crid": "CR69382", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "postbid_iframe", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "exp": 300, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047118", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/exemplary/multiple-impressions.json b/adapters/smaato/smaatotest/exemplary/multiple-impressions.json index 2901bcfc375..485636748aa 100644 --- a/adapters/smaato/smaatotest/exemplary/multiple-impressions.json +++ b/adapters/smaato/smaatotest/exemplary/multiple-impressions.json @@ -162,7 +162,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, @@ -268,7 +268,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/multiple-media-types-skadn.json b/adapters/smaato/smaatotest/exemplary/multiple-media-types-skadn.json new file mode 100644 index 00000000000..5fc29f28df1 --- /dev/null +++ b/adapters/smaato/smaatotest/exemplary/multiple-media-types-skadn.json @@ -0,0 +1,487 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "ext": { + "data": { + "keywords": "power tools", + "search": "drill", + "content": { + "userrating": 4 + } + } + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "video": { + "mimes": [ + "video/mp4", + "video/quicktime", + "video/3gpp", + "video/x-m4v" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ], + "ext": { + "rewarded": 0 + } + }, + "bidfloor": 0.00123, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString", + "data": { + "keywords": "a,b", + "gender": "M", + "yob": 1984, + "geo": { + "country": "ca" + } + } + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "bidfloor": 0.00123, + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "bidfloor": 0.00123, + "video": { + "w": 1024, + "h": 768, + "ext": { + "rewarded": 0 + }, + "mimes": [ + "video/mp4", + "video/quicktime", + "video/3gpp", + "video/x-m4v" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ] + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "ext": { + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "
\"\"\"\"
", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50, + "exp": 300, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "exp": 300, + "ext": { + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/exemplary/multiple-media-types.json b/adapters/smaato/smaatotest/exemplary/multiple-media-types.json index f5f4af3378a..bbca7d2eb61 100644 --- a/adapters/smaato/smaatotest/exemplary/multiple-media-types.json +++ b/adapters/smaato/smaatotest/exemplary/multiple-media-types.json @@ -152,7 +152,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, @@ -258,7 +258,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/native.json b/adapters/smaato/smaatotest/exemplary/native.json index 76534dd4f9d..00dfe9ec4af 100644 --- a/adapters/smaato/smaatotest/exemplary/native.json +++ b/adapters/smaato/smaatotest/exemplary/native.json @@ -102,7 +102,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner-app.json b/adapters/smaato/smaatotest/exemplary/simple-banner-app.json index 4f38f26180e..22f9c9b94d4 100644 --- a/adapters/smaato/smaatotest/exemplary/simple-banner-app.json +++ b/adapters/smaato/smaatotest/exemplary/simple-banner-app.json @@ -160,7 +160,7 @@ "keywords": "keywords" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia-app.json b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia-app.json index aac0198982d..3b054d43829 100644 --- a/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia-app.json +++ b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia-app.json @@ -164,7 +164,7 @@ "keywords": "keywords" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json index 42f516c7f23..268fd0d43d8 100644 --- a/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json +++ b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json @@ -129,7 +129,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner-skadn.json b/adapters/smaato/smaatotest/exemplary/simple-banner-skadn.json new file mode 100644 index 00000000000..33a7ef8e0a5 --- /dev/null +++ b/adapters/smaato/smaatotest/exemplary/simple-banner-skadn.json @@ -0,0 +1,283 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "ext": { + "data": { + "keywords": "power tools", + "search": "drill", + "content": { + "userrating": 4 + } + } + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "instl": 1, + "bidfloor": 0.00123, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString", + "data": { + "keywords": "a,b", + "gender": "M", + "yob": 1984, + "geo": { + "country": "ca" + } + } + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString", + "gpp": "gppString", + "gpp_sid": [ + 7 + ] + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "bidfloor": 0.00123, + "instl": 1, + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString", + "gpp": "gppString", + "gpp_sid": [ + 7 + ] + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "
\"\"\"\"
", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50, + "exp": 300, + "ext": { + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner.json b/adapters/smaato/smaatotest/exemplary/simple-banner.json index f6f1f9d746f..0596c47346f 100644 --- a/adapters/smaato/smaatotest/exemplary/simple-banner.json +++ b/adapters/smaato/smaatotest/exemplary/simple-banner.json @@ -133,7 +133,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/video-app.json b/adapters/smaato/smaatotest/exemplary/video-app.json index 33eb026c91e..79bd41ef5c4 100644 --- a/adapters/smaato/smaatotest/exemplary/video-app.json +++ b/adapters/smaato/smaatotest/exemplary/video-app.json @@ -165,7 +165,7 @@ "keywords": "keywords" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/exemplary/video.json b/adapters/smaato/smaatotest/exemplary/video.json index 17af4b367cf..42d56e60ac2 100644 --- a/adapters/smaato/smaatotest/exemplary/video.json +++ b/adapters/smaato/smaatotest/exemplary/video.json @@ -144,7 +144,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/adtype-header-response.json b/adapters/smaato/smaatotest/supplemental/adtype-header-response.json index d5bbd44061b..bebbc161e83 100644 --- a/adapters/smaato/smaatotest/supplemental/adtype-header-response.json +++ b/adapters/smaato/smaatotest/supplemental/adtype-header-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/bad-adm-response.json b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json index 0e898e8e18a..e45e8753a0b 100644 --- a/adapters/smaato/smaatotest/supplemental/bad-adm-response.json +++ b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, @@ -162,10 +162,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Invalid ad markup {\"badmedia\":{\"mediadata\":{\"content\":\"
hello
\", \"w\":350,\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}.", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/smaato/smaatotest/supplemental/bad-adtype-header-response.json b/adapters/smaato/smaatotest/supplemental/bad-adtype-header-response.json index 0b3e6dcd43d..053f0d3f603 100644 --- a/adapters/smaato/smaatotest/supplemental/bad-adtype-header-response.json +++ b/adapters/smaato/smaatotest/supplemental/bad-adtype-header-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, @@ -165,10 +165,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Unknown markup type something.", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/smaato/smaatotest/supplemental/bad-expires-header-response.json b/adapters/smaato/smaatotest/supplemental/bad-expires-header-response.json index 980dc3562b2..a990b92b732 100644 --- a/adapters/smaato/smaatotest/supplemental/bad-expires-header-response.json +++ b/adapters/smaato/smaatotest/supplemental/bad-expires-header-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/bad-skadn-format-request.json b/adapters/smaato/smaatotest/supplemental/bad-skadn-format-request.json new file mode 100644 index 00000000000..b996d0cf95e --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/bad-skadn-format-request.json @@ -0,0 +1,59 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder" + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + }, + "skadn": [] + } + }, + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282D", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + }, + "skadn": "" + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Invalid imp.ext.skadn.", + "comparison": "literal" + }, + { + "value": "Invalid imp.ext.skadn.", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-status-code-response.json b/adapters/smaato/smaatotest/supplemental/bad-status-code-response.json index 09c0d97e079..84b8fe9844b 100644 --- a/adapters/smaato/smaatotest/supplemental/bad-status-code-response.json +++ b/adapters/smaato/smaatotest/supplemental/bad-status-code-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/banner-w-and-h.json b/adapters/smaato/smaatotest/supplemental/banner-w-and-h.json index 4f1ddca4033..ca1a029a140 100644 --- a/adapters/smaato/smaatotest/supplemental/banner-w-and-h.json +++ b/adapters/smaato/smaatotest/supplemental/banner-w-and-h.json @@ -107,7 +107,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/expires-header-response.json b/adapters/smaato/smaatotest/supplemental/expires-header-response.json index 2002f8f3d66..86e1b101347 100644 --- a/adapters/smaato/smaatotest/supplemental/expires-header-response.json +++ b/adapters/smaato/smaatotest/supplemental/expires-header-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/no-bid-response.json b/adapters/smaato/smaatotest/supplemental/no-bid-response.json index de282d6db68..76ad48a4c88 100644 --- a/adapters/smaato/smaatotest/supplemental/no-bid-response.json +++ b/adapters/smaato/smaatotest/supplemental/no-bid-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/no-consent-info-request.json b/adapters/smaato/smaatotest/supplemental/no-consent-info-request.json index a2883b054fb..09b3a1fb1ca 100644 --- a/adapters/smaato/smaatotest/supplemental/no-consent-info-request.json +++ b/adapters/smaato/smaatotest/supplemental/no-consent-info-request.json @@ -72,7 +72,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/supplemental/outdated-expires-header-response.json b/adapters/smaato/smaatotest/supplemental/outdated-expires-header-response.json index 138e0ac8ba4..2c983fe2119 100644 --- a/adapters/smaato/smaatotest/supplemental/outdated-expires-header-response.json +++ b/adapters/smaato/smaatotest/supplemental/outdated-expires-header-response.json @@ -125,7 +125,7 @@ "keywords": "power tools" }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/video/multiple-adpods-skadn.json b/adapters/smaato/smaatotest/video/multiple-adpods-skadn.json new file mode 100644 index 00000000000..fe9e9d4deae --- /dev/null +++ b/adapters/smaato/smaatotest/video/multiple-adpods-skadn.json @@ -0,0 +1,813 @@ +{ + "mockBidRequest": { + "id": "447a0a1d-389d-4730-a418-3777e95de7bd", + "imp": [ + { + "id": "1_1", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "1_2", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047118", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "2_1", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047119", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "2_2", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047120", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "site": { + "publisher": { + "id": "1100042525" + }, + "content": { + "title": "a-title", + "season": "a-season", + "series": "a-series", + "episode": 1, + "len": 900, + "livestream": 1 + } + }, + "device": { + "ua": "test-user-agent" + }, + "user": { + "ext": { + "data": {} + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "447a0a1d-389d-4730-a418-3777e95de7bd", + "imp": [ + { + "id": "1_1", + "tagid": "400000001", + "video": { + "w": 1024, + "h": 768, + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ], + "sequence": 1, + "ext": { + "context": "adpod" + } + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "1_2", + "tagid": "400000001", + "video": { + "w": 1024, + "h": 768, + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ], + "sequence": 2, + "ext": { + "context": "adpod" + } + } + } + ], + "user": { + "ext": { + } + }, + "device": { + "ua": "test-user-agent" + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "content": { + "title": "a-title", + "season": "a-season", + "series": "a-series", + "episode": 1, + "len": 900, + "livestream": 1 + } + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_1", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB1"], + "ext": { + "duration": 5, + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_2", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB2"], + "ext": { + "duration": 30, + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "447a0a1d-389d-4730-a418-3777e95de7bd", + "imp": [ + { + "id": "2_1", + "tagid": "400000001", + "video": { + "w": 1024, + "h": 768, + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ], + "sequence": 1, + "ext": { + "context": "adpod" + } + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047119", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "2_2", + "tagid": "400000001", + "video": { + "w": 1024, + "h": 768, + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ], + "sequence": 2, + "ext": { + "context": "adpod" + } + } + } + ], + "user": { + "ext": { + } + }, + "device": { + "ua": "test-user-agent" + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "content": { + "title": "a-title", + "season": "a-season", + "series": "a-series", + "episode": 1, + "len": 900, + "livestream": 1 + } + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "2_1", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB1"], + "ext": { + "duration": 5, + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047119", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "2_2", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB2"], + "ext": { + "duration": 30, + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047119", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_1", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB1"], + "ext": { + "duration": 5, + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + }, + "exp": 300 + }, + "type": "video" + + }, + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_2", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB2"], + "ext": { + "duration": 30, + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + }, + "exp": 300 + }, + "type": "video" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "2_1", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB1"], + "ext": { + "duration": 5, + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047119", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + }, + "exp": 300 + }, + "type": "video" + }, + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "2_2", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB2"], + "ext": { + "duration": 30, + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047119", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + }, + "exp": 300 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/video/multiple-adpods.json b/adapters/smaato/smaatotest/video/multiple-adpods.json index 1f098c92ff2..03641b866bf 100644 --- a/adapters/smaato/smaatotest/video/multiple-adpods.json +++ b/adapters/smaato/smaatotest/video/multiple-adpods.json @@ -234,7 +234,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, @@ -386,7 +386,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/video/single-adpod-skadn.json b/adapters/smaato/smaatotest/video/single-adpod-skadn.json new file mode 100644 index 00000000000..2f0f5129963 --- /dev/null +++ b/adapters/smaato/smaatotest/video/single-adpod-skadn.json @@ -0,0 +1,426 @@ +{ + "mockBidRequest": { + "id": "447a0a1d-389d-4730-a418-3777e95de7bd", + "imp": [ + { + "id": "1_1", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "bidfloor": 0.00123, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "1_2", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "bidfloor": 0.00123, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047118", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + } + ], + "site": { + "publisher": { + "id": "1100042525" + }, + "content": { + "title": "a-title", + "season": "a-season", + "series": "a-series", + "episode": 1, + "len": 900, + "livestream": 1 + } + }, + "device": { + "ua": "test-user-agent" + }, + "user": { + "ext": { + "data": {} + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "447a0a1d-389d-4730-a418-3777e95de7bd", + "imp": [ + { + "id": "1_1", + "tagid": "400000001", + "bidfloor": 0.00123, + "video": { + "w": 1024, + "h": 768, + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ], + "sequence": 1, + "ext": { + "context": "adpod" + } + }, + "ext": { + "skadn": { + "versions": ["2.0", "2.1", "2.2", "3.0", "4.0"], + "sourceapp": "880047117", + "productpage": 1, + "skadnetlist": { + "max": 306, + "excl": [2, 8, 10, 55], + "addl": [ + "cdkw7geqsh.skadnetwork", + "qyJfv329m4.skadnetwork" + ] + } + } + } + }, + { + "id": "1_2", + "tagid": "400000001", + "bidfloor": 0.00123, + "video": { + "w": 1024, + "h": 768, + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "startdelay": 0, + "linearity": 1, + "maxduration": 30, + "skip": 1, + "protocols": [ + 7 + ], + "skipmin": 5, + "api": [ + 7 + ], + "sequence": 2, + "ext": { + "context": "adpod" + } + } + } + ], + "user": { + "ext": { + } + }, + "device": { + "ua": "test-user-agent" + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "content": { + "title": "a-title", + "season": "a-season", + "series": "a-series", + "episode": 1, + "len": 900, + "livestream": 1 + } + }, + "ext": { + "client": "prebid_server_0.6" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_1", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB1"], + "ext": { + "duration": 5, + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + }, + { + "adm": "", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_2", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB2"], + "ext": { + "duration": 30, + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + } + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_1", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB1"], + "ext": { + "duration": 5, + "skadn": { + "version": "2.2", + "network": "cdkw7geqsh.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + }, + "exp": 300 + }, + "type": "video" + }, + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1_2", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768, + "cat": ["IAB2"], + "ext": { + "duration": 30, + "skadn": { + "version": "2.2", + "network": "qyJfv329m4.skadnetwork", + "campaign": "45", + "itunesitem": "123456789", + "sourceapp": "880047117", + "productpageid": "45812c9b-c296-43d3-c6a0-c5a02f74bf6e", + "fidelities": [ + { + "fidelity": 0, + "signature": "MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg==", + "nonce": "473b1a16-b4ef-43ad-9591-fcf3aefa82a7", + "timestamp": "1594406341" + }, + { + "fidelity": 1, + "signature": "GRlMDktMmE5Zi00ZGMzLWE0ZDEtNTQ0YzQwMmU5MDk1IiwKICAgICAgICAgICAgICAgICAgInRpbWVzdGTk0NDA2MzQyIg==", + "nonce": "e650de09-2a9f-4dc3-a4d1-544c402e9095", + "timestamp": "1594406342" + } + ] + } + }, + "exp": 300 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/video/single-adpod.json b/adapters/smaato/smaatotest/video/single-adpod.json index 404e3551556..3029782db5a 100644 --- a/adapters/smaato/smaatotest/video/single-adpod.json +++ b/adapters/smaato/smaatotest/video/single-adpod.json @@ -180,7 +180,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/videosupplemental/bad-adm-response.json b/adapters/smaato/smaatotest/videosupplemental/bad-adm-response.json index 22e40ba1c4e..00d715082b5 100644 --- a/adapters/smaato/smaatotest/videosupplemental/bad-adm-response.json +++ b/adapters/smaato/smaatotest/videosupplemental/bad-adm-response.json @@ -176,7 +176,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/videosupplemental/bad-bid-ext-response.json b/adapters/smaato/smaatotest/videosupplemental/bad-bid-ext-response.json index ca6cc068066..f3707dead17 100644 --- a/adapters/smaato/smaatotest/videosupplemental/bad-bid-ext-response.json +++ b/adapters/smaato/smaatotest/videosupplemental/bad-bid-ext-response.json @@ -176,7 +176,7 @@ } }, "ext": { - "client": "prebid_server_0.5" + "client": "prebid_server_0.6" } } }, diff --git a/adapters/smaato/smaatotest/videosupplemental/bad-skadn-format-request.json b/adapters/smaato/smaatotest/videosupplemental/bad-skadn-format-request.json new file mode 100644 index 00000000000..35d33cf7fee --- /dev/null +++ b/adapters/smaato/smaatotest/videosupplemental/bad-skadn-format-request.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "447a0a1d-389d-4730-a418-3777e95de7bd", + "imp": [ + { + "id": "1_1", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "bidfloor": 0.00123, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adbreakId": "400000001" + }, + "skadn": [] + } + }, + { + "id": "1_2", + "video": { + "mimes": [ + "video/mp4", + "video/3gpp" + ], + "minduration": 5, + "maxduration": 30, + "protocols": [ + 7 + ], + "w": 1024, + "h": 768, + "startdelay": 0, + "linearity": 1, + "skip": 1, + "skipmin": 5, + "api": [ + 7 + ] + }, + "bidfloor": 0.00123 + } + ], + "site": { + "publisher": { + "id": "1100042525" + }, + "content": { + "title": "a-title", + "season": "a-season", + "series": "a-series", + "episode": 1, + "len": 900, + "livestream": 1 + } + }, + "device": { + "ua": "test-user-agent" + }, + "user": { + "ext": { + "data": {} + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Invalid imp.ext.skadn.", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/sovrn/sovrntest/supplemental/invalid-imp-id.json b/adapters/sovrn/sovrntest/supplemental/invalid-imp-id.json index 874844dc8bd..e3b2c17c8c3 100644 --- a/adapters/sovrn/sovrntest/supplemental/invalid-imp-id.json +++ b/adapters/sovrn/sovrntest/supplemental/invalid-imp-id.json @@ -113,7 +113,7 @@ } } ], - + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Imp ID test-imp-idx in bid didn't match with any imp in the original request", diff --git a/adapters/sspBC/sspbctest/supplemental/bad_response_without_adm.json b/adapters/sspBC/sspbctest/supplemental/bad_response_without_adm.json index 46774f3051c..f83584999ab 100644 --- a/adapters/sspBC/sspbctest/supplemental/bad_response_without_adm.json +++ b/adapters/sspBC/sspbctest/supplemental/bad_response_without_adm.json @@ -120,10 +120,11 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "bid format is not supported", "comparison": "literal" } ] -} \ No newline at end of file +} diff --git a/adapters/taboola/taboola.go b/adapters/taboola/taboola.go index cb47fc9b250..087f661104f 100644 --- a/adapters/taboola/taboola.go +++ b/adapters/taboola/taboola.go @@ -253,8 +253,7 @@ func makeRequestExt(pageType string) (json.RawMessage, error) { requestExtJson, err := json.Marshal(requestExt) if err != nil { - fmt.Errorf("could not marshal %s", requestExt) - return nil, err + return nil, fmt.Errorf("could not marshal %s, err: %s", requestExt, err) } return requestExtJson, nil diff --git a/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json b/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json index 7373d05c774..3cc915cec6f 100644 --- a/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json +++ b/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json @@ -123,6 +123,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "value": "Failed to find banner/native impression \"incorrect-impression-id\" ", diff --git a/adapters/tpmn/params_test.go b/adapters/tpmn/params_test.go new file mode 100644 index 00000000000..7bd7c478638 --- /dev/null +++ b/adapters/tpmn/params_test.go @@ -0,0 +1,44 @@ +package tpmn + +import ( + "encoding/json" + "github.com/prebid/prebid-server/openrtb_ext" + "testing" +) + +var validParams = []string{ + `{"inventoryId": 10000001}`, +} + +var invalidParams = []string{ + `{"inventoryId": "00000001"}`, + `{"inventoryid": 100000000}`, +} + +// TestValidParams makes sure that the tpmn schema accepts all imp.ext fields which we intend to support. +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderTpmn, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected TPMN params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the tpmn schema rejects all the imp.ext fields we don't support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderTpmn, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/tpmn/tpmn.go b/adapters/tpmn/tpmn.go new file mode 100644 index 00000000000..7afe94e5f79 --- /dev/null +++ b/adapters/tpmn/tpmn.go @@ -0,0 +1,126 @@ +package tpmn + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TpmnAdapter struct +type adapter struct { + uri string +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids from TpmnBidder. +func (rcv *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + validImps, errs := getValidImpressions(request, reqInfo) + if len(validImps) == 0 { + return nil, errs + } + + request.Imp = validImps + + requestBodyJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return []*adapters.RequestData{{ + Method: http.MethodPost, + Uri: rcv.uri, + Body: requestBodyJSON, + Headers: headers, + }}, errs +} + +// getValidImpressions validate imps and check for bid floor currency. Convert to EUR if necessary +func getValidImpressions(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]openrtb2.Imp, []error) { + var errs []error + var validImps []openrtb2.Imp + + for _, imp := range request.Imp { + if err := preprocessBidFloorCurrency(&imp, reqInfo); err != nil { + errs = append(errs, err) + continue + } + validImps = append(validImps, imp) + } + return validImps, errs +} + +func preprocessBidFloorCurrency(imp *openrtb2.Imp, reqInfo *adapters.ExtraRequestInfo) error { + // we expect every currency related data to be EUR + if imp.BidFloor > 0 && strings.ToUpper(imp.BidFloorCur) != "USD" && imp.BidFloorCur != "" { + if convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD"); err != nil { + return err + } else { + imp.BidFloor = convertedValue + } + } + imp.BidFloorCur = "USD" + return nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{fmt.Errorf("bid response unmarshal: %v", err)} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForImp(bid) + if err != nil { + return nil, []error{err} + } + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("unsupported MType %d", bid.MType) + } +} + +// Builder builds a new instance of the TpmnBidder adapter for the given bidder with the given config. +func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + uri: config.Endpoint, + } + return bidder, nil +} diff --git a/adapters/tpmn/tpmn_test.go b/adapters/tpmn/tpmn_test.go new file mode 100644 index 00000000000..6fbd85936f1 --- /dev/null +++ b/adapters/tpmn/tpmn_test.go @@ -0,0 +1,20 @@ +package tpmn + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderTpmn, config.Adapter{ + Endpoint: "https://gat.tpmn.io/ortb/pbs_bidder"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "tpmntest", bidder) +} diff --git a/adapters/tpmn/tpmntest/exemplary/simple-banner.json b/adapters/tpmn/tpmntest/exemplary/simple-banner.json new file mode 100644 index 00000000000..197d03b174e --- /dev/null +++ b/adapters/tpmn/tpmntest/exemplary/simple-banner.json @@ -0,0 +1,135 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "00000001", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "00000001", + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "tpmn" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/tpmn/tpmntest/exemplary/simple-native.json b/adapters/tpmn/tpmntest/exemplary/simple-native.json new file mode 100644 index 00000000000..1880c74ac7e --- /dev/null +++ b/adapters/tpmn/tpmntest/exemplary/simple-native.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "mtype": 4, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "tpmn" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "mtype": 4, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/tpmn/tpmntest/exemplary/simple-site-banner.json b/adapters/tpmn/tpmntest/exemplary/simple-site-banner.json new file mode 100644 index 00000000000..8f7c5d59301 --- /dev/null +++ b/adapters/tpmn/tpmntest/exemplary/simple-site-banner.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "tpmn" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} + \ No newline at end of file diff --git a/adapters/tpmn/tpmntest/exemplary/simple-site-native.json b/adapters/tpmn/tpmntest/exemplary/simple-site-native.json new file mode 100644 index 00000000000..20e6c23e966 --- /dev/null +++ b/adapters/tpmn/tpmntest/exemplary/simple-site-native.json @@ -0,0 +1,104 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"123.123.123.123" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"123.123.123.123" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "tpmn", + "bid": [{ + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-adm", + "crid": "test-crid", + "mtype": 4 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-adm", + "crid": "test-crid", + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/tpmn/tpmntest/exemplary/simple-site-video.json b/adapters/tpmn/tpmntest/exemplary/simple-site-video.json new file mode 100644 index 00000000000..7b2375cd07a --- /dev/null +++ b/adapters/tpmn/tpmntest/exemplary/simple-site-video.json @@ -0,0 +1,128 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"123.123.123.123" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"123.123.123.123" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 576 + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "tpmn", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-adm", + "crid": "test-crid", + "w": 1024, + "h": 576, + "mtype": 2 + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-adm", + "crid": "test-crid", + "w": 1024, + "h": 576, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/tpmn/tpmntest/exemplary/simple-video.json b/adapters/tpmn/tpmntest/exemplary/simple-video.json new file mode 100644 index 00000000000..505b6167069 --- /dev/null +++ b/adapters/tpmn/tpmntest/exemplary/simple-video.json @@ -0,0 +1,130 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "tpmn" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "mtype": 2, + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/tpmn/tpmntest/supplemental/bad-imp-ext.json b/adapters/tpmn/tpmntest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..3bbb23f95a4 --- /dev/null +++ b/adapters/tpmn/tpmntest/supplemental/bad-imp-ext.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "00000001", + "ext": { + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "00000001", + "bidfloorcur": "USD", + "ext": { + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 204, + "body": { + } + } + } + ], + "expectedBidResponses": [ + ] +} diff --git a/adapters/tpmn/tpmntest/supplemental/bad_response.json b/adapters/tpmn/tpmntest/supplemental/bad_response.json new file mode 100644 index 00000000000..12c3a72d49f --- /dev/null +++ b/adapters/tpmn/tpmntest/supplemental/bad_response.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "bid response unmarshal: json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/tpmn/tpmntest/supplemental/no-imp-ext.json b/adapters/tpmn/tpmntest/supplemental/no-imp-ext.json new file mode 100644 index 00000000000..8bcbb93d004 --- /dev/null +++ b/adapters/tpmn/tpmntest/supplemental/no-imp-ext.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": "" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": "", + "bidfloorcur": "USD" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "bid response unmarshal: json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} + \ No newline at end of file diff --git a/adapters/tpmn/tpmntest/supplemental/status-204.json b/adapters/tpmn/tpmntest/supplemental/status-204.json new file mode 100644 index 00000000000..fdcd3f7fd55 --- /dev/null +++ b/adapters/tpmn/tpmntest/supplemental/status-204.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/tpmn/tpmntest/supplemental/status-404.json b/adapters/tpmn/tpmntest/supplemental/status-404.json new file mode 100644 index 00000000000..74ced15217c --- /dev/null +++ b/adapters/tpmn/tpmntest/supplemental/status-404.json @@ -0,0 +1,86 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://gat.tpmn.io/ortb/pbs_bidder", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "inventoryId": 10000001 + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/undertone/undertone.go b/adapters/undertone/undertone.go index 62934fa5954..7f8dde35abb 100644 --- a/adapters/undertone/undertone.go +++ b/adapters/undertone/undertone.go @@ -26,6 +26,11 @@ type undertoneParams struct { Version string `json:"version"` } +type impExt struct { + Bidder *openrtb_ext.ExtImpUndertone `json:"bidder,omitempty"` + Gpid string `json:"gpid,omitempty"` +} + func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &adapter{ endpoint: config.Endpoint, @@ -158,24 +163,29 @@ func getImpsAndPublisherId(bidRequest *openrtb2.BidRequest) ([]openrtb2.Imp, int var validImps []openrtb2.Imp for _, imp := range bidRequest.Imp { - var extImpBidder adapters.ExtImpBidder - if err := json.Unmarshal(imp.Ext, &extImpBidder); err != nil { - errs = append(errs, getInvalidImpErr(imp.ID, err)) - continue - } - - var extImpUndertone openrtb_ext.ExtImpUndertone - if err := json.Unmarshal(extImpBidder.Bidder, &extImpUndertone); err != nil { + var ext impExt + if err := json.Unmarshal(imp.Ext, &ext); err != nil { errs = append(errs, getInvalidImpErr(imp.ID, err)) continue } if publisherId == 0 { - publisherId = extImpUndertone.PublisherID + publisherId = ext.Bidder.PublisherID } - imp.TagID = strconv.Itoa(extImpUndertone.PlacementID) + imp.TagID = strconv.Itoa(ext.Bidder.PlacementID) imp.Ext = nil + + if ext.Gpid != "" { + ext.Bidder = nil + impExtJson, err := json.Marshal(&ext) + if err != nil { + errs = append(errs, getInvalidImpErr(imp.ID, err)) + continue + } + imp.Ext = impExtJson + } + validImps = append(validImps, imp) } diff --git a/adapters/undertone/undertonetest/exemplary/multi-imp-app-request.json b/adapters/undertone/undertonetest/exemplary/multi-imp-app-request.json index e6b4a83ae3a..71ad2ae199d 100644 --- a/adapters/undertone/undertonetest/exemplary/multi-imp-app-request.json +++ b/adapters/undertone/undertonetest/exemplary/multi-imp-app-request.json @@ -54,7 +54,8 @@ "bidder": { "publisherId": 1234, "placementId": 123456 - } + }, + "gpid": "gpid-value" } } ], @@ -116,7 +117,10 @@ 4 ] }, - "tagid": "123456" + "tagid": "123456", + "ext": { + "gpid": "gpid-value" + } } ], "app": { diff --git a/adapters/undertone/undertonetest/exemplary/multi-imp-site-request.json b/adapters/undertone/undertonetest/exemplary/multi-imp-site-request.json index 0de38caa986..9e59ffbca63 100644 --- a/adapters/undertone/undertonetest/exemplary/multi-imp-site-request.json +++ b/adapters/undertone/undertonetest/exemplary/multi-imp-site-request.json @@ -20,7 +20,8 @@ "bidder": { "publisherId": 1234, "placementId": 12345 - } + }, + "gpid": "gpid-value" } }, { @@ -87,7 +88,10 @@ } ] }, - "tagid": "12345" + "tagid": "12345", + "ext": { + "gpid": "gpid-value" + } }, { "id": "test-imp-video-id", diff --git a/adapters/unruly/unrulytest/supplemental/no-matching-impid.json b/adapters/unruly/unrulytest/supplemental/no-matching-impid.json index d3519db3635..efabbe64cfe 100644 --- a/adapters/unruly/unrulytest/supplemental/no-matching-impid.json +++ b/adapters/unruly/unrulytest/supplemental/no-matching-impid.json @@ -87,6 +87,7 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { "comparison": "literal", diff --git a/adapters/vidoomy/vidoomy.go b/adapters/vidoomy/vidoomy.go index 74f4a5436cb..7e7e9d64eb3 100644 --- a/adapters/vidoomy/vidoomy.go +++ b/adapters/vidoomy/vidoomy.go @@ -17,7 +17,7 @@ type adapter struct { endpoint string } -func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var errors []error reqs := make([]*adapters.RequestData, 0, len(request.Imp)) @@ -103,7 +103,7 @@ func changeRequestForBidService(request *openrtb2.BidRequest) error { return nil } -func (a adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { return nil, nil } diff --git a/adapters/vidoomy/vidoomytest/supplemental/simple-banner-no-response.json b/adapters/vidoomy/vidoomytest/supplemental/simple-banner-no-response.json index 0964351a483..b765252fb7a 100644 --- a/adapters/vidoomy/vidoomytest/supplemental/simple-banner-no-response.json +++ b/adapters/vidoomy/vidoomytest/supplemental/simple-banner-no-response.json @@ -77,9 +77,5 @@ "body": {} } } - ], - - "expectedBidResponses": [ - {} ] } diff --git a/adapters/vox/params_test.go b/adapters/vox/params_test.go new file mode 100644 index 00000000000..be148d3b32d --- /dev/null +++ b/adapters/vox/params_test.go @@ -0,0 +1,56 @@ +package vox + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderVox, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderVox, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "64be6fe6685a271d37e900d2"}`, + `{"placementId": "Any String Basically"}`, + `{"placementId":""}`, + `{"placementId":"id", "imageUrl":"http://site.com/img1.png"}`, + `{"placementId":"id", "imageUrl":"http://site.com/img1.png", "displaySizes":["123x90", "1x1", "987x1111"]}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"placementId": null}`, + `{"placementId": 3.1415}`, + `{"placementId": true}`, + `{"placementId": false}`, + `{"placementId":"id", "imageUrl": null}`, + `{"placementId":"id", "imageUrl": true}`, + `{"placementId":"id", "imageUrl": []}`, + `{"placementId":"id", "imageUrl": "http://some.url", "displaySizes": null}`, + `{"placementId":"id", "imageUrl": "http://some.url", "displaySizes": {}}`, + `{"placementId":"id", "imageUrl": "http://some.url", "displaySizes": "String"}`, +} diff --git a/adapters/vox/vox.go b/adapters/vox/vox.go new file mode 100644 index 00000000000..0b56fcbf9d7 --- /dev/null +++ b/adapters/vox/vox.go @@ -0,0 +1,86 @@ +package vox + +import ( + "encoding/json" + "fmt" + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + requestJSON, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJSON, + } + + return []*adapters.RequestData{requestData}, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + + return bidResponse, nil +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("Unable to fetch mediaType in multi-format: %s", bid.ImpID) + } +} diff --git a/adapters/vox/vox_test.go b/adapters/vox/vox_test.go new file mode 100644 index 00000000000..95d11a8ad79 --- /dev/null +++ b/adapters/vox/vox_test.go @@ -0,0 +1,21 @@ +package vox + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderVox, config.Adapter{ + Endpoint: "http://somecoolurlfor.vox"}, + config.Server{ExternalUrl: "http://somecoolurlfor.vox", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error: %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "voxtest", bidder) +} diff --git a/adapters/vox/voxtest/exemplary/banner.json b/adapters/vox/voxtest/exemplary/banner.json new file mode 100644 index 00000000000..898fcea4826 --- /dev/null +++ b/adapters/vox/voxtest/exemplary/banner.json @@ -0,0 +1,82 @@ +{ + "mockBidRequest": { + "id": "cf4b0abb-7ccc-4057-9914-c85f467260c6", + "site": { + "page": "http://some.site/url/page?id=338" + }, + "cur": [ "USD" ], + "imp": [{ + "id": "8a7510f9-0ca1-44c4-a8c6-1ce639b5eef9", + "banner": { "w": 100, "h": 100 }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "placementId":"64b939146d66df22ccae95c5" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://somecoolurlfor.vox", + "headers": {}, + "body": { + "id": "cf4b0abb-7ccc-4057-9914-c85f467260c6", + "site": { + "page": "http://some.site/url/page?id=338" + }, + "imp": [ + { + "id": "8a7510f9-0ca1-44c4-a8c6-1ce639b5eef9", + "banner": { + "w": 100, + "h": 100 + }, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "placementId": "64b939146d66df22ccae95c5" + } + } + } + ], + "cur": [ "USD" ] + } + }, + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "id": "cf4b0abb-7ccc-4057-9914-c85f467260c6", + "cur": "USD", + "seatbid": [{ + "bid": [{ + "id": "e6a143f3-5176-4607-b09e-0e67e358b0b6", + "impid": "8a7510f9-0ca1-44c4-a8c6-1ce639b5eef9", + "price": 3.1415, + "adm": "

Hi, there

", + "w": 50, + "h": 50, + "mtype": 1 + }] + }] + }} + }], + + "expectedBidResponses": [{ + "bids": [{ + "currency": "USD", + "bid": { + "id": "e6a143f3-5176-4607-b09e-0e67e358b0b6", + "impid": "8a7510f9-0ca1-44c4-a8c6-1ce639b5eef9", + "price": 3.1415, + "adm": "

Hi, there

", + "w": 50, + "h": 50, + "mtype": 1 + }, + "type": "banner" + }] + }] +} \ No newline at end of file diff --git a/adapters/vox/voxtest/exemplary/video.json b/adapters/vox/voxtest/exemplary/video.json new file mode 100644 index 00000000000..37464da1aec --- /dev/null +++ b/adapters/vox/voxtest/exemplary/video.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "9a4bdb67-43ef-488f-937d-cd01e9dce43d", + "site": { + "page": "http://blog.com/article/1" + }, + "cur": ["PLN"], + "imp": [{ + "id": "d190d6f3-5264-4df5-91b2-8a9c72cbeb6a", + "bidfloorcur": "PLN", + "video": { + "mimes": ["video/mp4"], + "minduration": 5, + "maxduration": 15, + "w": 1280, + "h": 720 + }, + "ext": { + "bidder": { + "placementId":"64b9486efecad2330718e233" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://somecoolurlfor.vox", + "header": {}, + "body": { + "id": "9a4bdb67-43ef-488f-937d-cd01e9dce43d", + "site": { + "page": "http://blog.com/article/1" + }, + "cur": [ "PLN" ], + "imp": [{ + "id": "d190d6f3-5264-4df5-91b2-8a9c72cbeb6a", + "bidfloorcur": "PLN", + "video": { + "mimes": ["video/mp4"], + "minduration": 5, + "maxduration": 15, + "w": 1280, + "h": 720 + }, + "ext": { + "bidder": { + "placementId":"64b9486efecad2330718e233" + } + } + }] + } + }, + + "mockResponse": { + "status": 200, + "headers": {}, + "body": { + "id":"d64fdeae-cb1c-4322-8fb6-18f5ec49bb76", + "cur": "PLN", + "seatbid": [{ + "bid": [{ + "id": "05349123-29e2-4be0-b662-48914f75ebe1", + "impid": "d190d6f3-5264-4df5-91b2-8a9c72cbeb6a", + "price": 2.149, + "adm": "...", + "mtype": 2 + }] + }] + } + } + }], + + "expectedBidResponses": [{ + "bids": [{ + "currency": "PLN", + "bid": { + "id": "05349123-29e2-4be0-b662-48914f75ebe1", + "impid": "d190d6f3-5264-4df5-91b2-8a9c72cbeb6a", + "price": 2.149, + "adm": "...", + "mtype": 2 + }, + "type": "video" + }] + }] +} \ No newline at end of file diff --git a/adapters/vox/voxtest/supplemental/response_204_to_nocontent.json b/adapters/vox/voxtest/supplemental/response_204_to_nocontent.json new file mode 100644 index 00000000000..0a49b85d612 --- /dev/null +++ b/adapters/vox/voxtest/supplemental/response_204_to_nocontent.json @@ -0,0 +1,45 @@ +{ + "mockBidRequest": { + "id": "Some Id (445455)", + "cur": ["AUD"], + "imp": [{ + "id": "Impression id #1", + "banner": {}, + "bidfloorcur": "AUD", + "ext": { + "bidder": { + "placementId": "64b94ad4ed136806629dd51c" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://somecoolurlfor.vox", + "headers": {}, + "body": { + "id": "Some Id (445455)", + "cur": ["AUD"], + "imp": [{ + "id": "Impression id #1", + "banner": {}, + "bidfloorcur": "AUD", + "ext": { + "bidder": { + "placementId": "64b94ad4ed136806629dd51c" + } + } + }] + } + }, + + "mockResponse": { + "status": 204, + "headers": {}, + "body": {} + } + }], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/vox/voxtest/supplemental/response_500_to_error.json b/adapters/vox/voxtest/supplemental/response_500_to_error.json new file mode 100644 index 00000000000..f245ef45c99 --- /dev/null +++ b/adapters/vox/voxtest/supplemental/response_500_to_error.json @@ -0,0 +1,56 @@ +{ + "mockBidRequest": { + "id": "It is hard to make up ids.", + "site": { + "page": "http://catsanddogs.com/article/887" + }, + "cur": ["USD"], + "imp": [{ + "id": "Impression id #4", + "bidfloorcur": "USD", + "video": { "w": 1280, "h": 720, "mimes": ["video/mp4"] }, + "ext": { + "bidder": { + "placementId": "64b94ca2b5eb3605e0a0e3be" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://somecoolurlfor.vox", + "heades": {}, + "body": { + "id": "It is hard to make up ids.", + "site": { + "page": "http://catsanddogs.com/article/887" + }, + "cur": ["USD"], + "imp": [{ + "id": "Impression id #4", + "bidfloorcur": "USD", + "video": { "w": 1280, "h": 720, "mimes": ["video/mp4"] }, + "ext": { + "bidder": { + "placementId": "64b94ca2b5eb3605e0a0e3be" + } + } + }] + } + }, + + "mockResponse": { + "status": 500, + "headers": {}, + "body": {} + } + }], + + "expectedBidResponses": [], + + "expectedMakeBidsErrors": [{ + "value": "Unexpected status code: 500", + "comparison": "regex" + }] +} \ No newline at end of file diff --git a/adapters/vrtcal/vrtcal.go b/adapters/vrtcal/vrtcal.go index 65e20b2116f..ab47eddb441 100644 --- a/adapters/vrtcal/vrtcal.go +++ b/adapters/vrtcal/vrtcal.go @@ -71,7 +71,7 @@ func (a *VrtcalAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR var errs []error for _, sb := range bidResp.SeatBid { for i := range sb.Bid { - bidType, err := getReturnTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + bidType, err := getReturnTypeForImp(sb.Bid[i].MType) if err == nil { bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &sb.Bid[i], @@ -94,25 +94,15 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co return bidder, nil } -func getReturnTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { - for _, imp := range imps { - if imp.ID == impID { - if imp.Banner != nil { - return openrtb_ext.BidTypeBanner, nil - } - - if imp.Video != nil { - return openrtb_ext.BidTypeVideo, nil - } - - return "", &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Unsupported return type for ID: \"%s\"", impID), - } - } - } - - //Failsafe default in case impression ID is not found - return "", &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Failed to find impression for ID: \"%s\"", impID), +func getReturnTypeForImp(mType openrtb2.MarkupType) (openrtb_ext.BidType, error) { + if mType == openrtb2.MarkupBanner { + return openrtb_ext.BidTypeBanner, nil + } else if mType == openrtb2.MarkupVideo { + return openrtb_ext.BidTypeVideo, nil + } else if mType == openrtb2.MarkupNative { + return openrtb_ext.BidTypeNative, nil + } else { + return "", &errortypes.BadServerResponse{ + Message: "Unsupported return type"} } } diff --git a/adapters/vrtcal/vrtcaltest/exemplary/simple-banner.json b/adapters/vrtcal/vrtcaltest/exemplary/simple-banner.json index 81808a3b0f2..27682f7ee21 100644 --- a/adapters/vrtcal/vrtcaltest/exemplary/simple-banner.json +++ b/adapters/vrtcal/vrtcaltest/exemplary/simple-banner.json @@ -68,7 +68,8 @@ "adm": "some-test-ad", "crid": "crid_10", "h": 250, - "w": 300 + "w": 300, + "mtype":1 } ] } @@ -90,7 +91,8 @@ "adm": "some-test-ad", "crid": "crid_10", "w": 300, - "h": 250 + "h": 250, + "mtype":1 }, "type": "banner" } diff --git a/adapters/vrtcal/vrtcaltest/exemplary/simple-native.json b/adapters/vrtcal/vrtcaltest/exemplary/simple-native.json new file mode 100644 index 00000000000..3c63914c3a6 --- /dev/null +++ b/adapters/vrtcal/vrtcaltest/exemplary/simple-native.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + + "native": { + "ver": "1.2", + "request": "{\"assets\":[{\"id\":1,\"img\":{\"ext\":{\"image1\":\"image2\"},\"h\": 250,\"mimes\":[\"image\/gif\",\"image\/png\"],\"type\":3,\"w\":300},\"required\":1}]}" + }, + "ext": { + "bidder": { + "Just_an_unused_vrtcal_param": "unused_data_for_this" + } + } + } + ], + "app": { + "id": "fake-app-id" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1804", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.2", + "request": "{\"assets\":[{\"id\":1,\"img\":{\"ext\":{\"image1\":\"image2\"},\"h\": 250,\"mimes\":[\"image\/gif\",\"image\/png\"],\"type\":3,\"w\":300},\"required\":1}]}" + }, + "ext": { + "bidder": { + "Just_an_unused_vrtcal_param": "unused_data_for_this" + } + } + } + ], + "app": { + "id": "fake-app-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "vrtcal", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-native-ad", + "crid": "crid_10", + "mtype":4 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-native-ad", + "crid": "crid_10", + "mtype":4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/vrtcal/vrtcaltest/exemplary/simple-video.json b/adapters/vrtcal/vrtcaltest/exemplary/simple-video.json index 365c892b87f..2baaf1aa4ba 100644 --- a/adapters/vrtcal/vrtcaltest/exemplary/simple-video.json +++ b/adapters/vrtcal/vrtcaltest/exemplary/simple-video.json @@ -64,7 +64,8 @@ "adm": "some-test-video-ad", "crid": "crid_10", "h": 250, - "w": 300 + "w": 300, + "mtype":2 } ] } @@ -86,7 +87,8 @@ "adm": "some-test-video-ad", "crid": "crid_10", "w": 300, - "h": 250 + "h": 250, + "mtype":2 }, "type": "video" } diff --git a/adapters/vrtcal/vrtcaltest/exemplary/web-simple-banner.json b/adapters/vrtcal/vrtcaltest/exemplary/web-simple-banner.json index 62fd90cfc61..eb7d2310d63 100644 --- a/adapters/vrtcal/vrtcaltest/exemplary/web-simple-banner.json +++ b/adapters/vrtcal/vrtcaltest/exemplary/web-simple-banner.json @@ -70,7 +70,8 @@ "adm": "some-test-ad", "crid": "crid_10", "h": 250, - "w": 300 + "w": 300, + "mtype":1 } ] } @@ -92,7 +93,8 @@ "adm": "some-test-ad", "crid": "crid_10", "w": 300, - "h": 250 + "h": 250, + "mtype":1 }, "type": "banner" } diff --git a/adapters/vrtcal/vrtcaltest/exemplary/web-simple-native.json b/adapters/vrtcal/vrtcaltest/exemplary/web-simple-native.json new file mode 100644 index 00000000000..6b416ab0726 --- /dev/null +++ b/adapters/vrtcal/vrtcaltest/exemplary/web-simple-native.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.2", + "request": "{\"assets\":[{\"id\":1,\"img\":{\"ext\":{\"image1\":\"image2\"},\"h\": 250,\"mimes\":[\"image\/gif\",\"image\/png\"],\"type\":3,\"w\":300},\"required\":1}]}" + }, + "ext": { + "bidder": { + "Just_an_unused_vrtcal_param": "unused_data_for_this" + } + } + } + ], + "site": { + "id": "fake-site-id", + "page": "http://www.vrtcal.com" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1804", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.2", + "request": "{\"assets\":[{\"id\":1,\"img\":{\"ext\":{\"image1\":\"image2\"},\"h\": 250,\"mimes\":[\"image\/gif\",\"image\/png\"],\"type\":3,\"w\":300},\"required\":1}]}" + }, + "ext": { + "bidder": { + "Just_an_unused_vrtcal_param": "unused_data_for_this" + } + } + } + ], + "site": { + "id": "fake-site-id", + "page": "http://www.vrtcal.com" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "vrtcal", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-native-ad", + "crid": "crid_10", + "mtype":4 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-native-ad", + "crid": "crid_10", + "mtype":4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/vrtcal/vrtcaltest/exemplary/web-simple-video.json b/adapters/vrtcal/vrtcaltest/exemplary/web-simple-video.json index f70c547a040..96ffd7099d1 100644 --- a/adapters/vrtcal/vrtcaltest/exemplary/web-simple-video.json +++ b/adapters/vrtcal/vrtcaltest/exemplary/web-simple-video.json @@ -66,7 +66,8 @@ "adm": "some-test-video-ad", "crid": "crid_10", "h": 250, - "w": 300 + "w": 300, + "mtype":2 } ] } @@ -88,7 +89,8 @@ "adm": "some-test-video-ad", "crid": "crid_10", "w": 300, - "h": 250 + "h": 250, + "mtype":2 }, "type": "video" } diff --git a/adapters/vrtcal/vrtcaltest/supplemental/unmatched_response_impression_id.json b/adapters/vrtcal/vrtcaltest/supplemental/unsupported_return_type.json similarity index 89% rename from adapters/vrtcal/vrtcaltest/supplemental/unmatched_response_impression_id.json rename to adapters/vrtcal/vrtcaltest/supplemental/unsupported_return_type.json index 42cf5f1a0f2..e2fc18f373e 100644 --- a/adapters/vrtcal/vrtcaltest/supplemental/unmatched_response_impression_id.json +++ b/adapters/vrtcal/vrtcaltest/supplemental/unsupported_return_type.json @@ -63,13 +63,14 @@ "bid": [ { "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "WRONG-IMPRESSION_ID", + "impid": "test-imp-id", "price": 0.500000, "adm": "some-test-ad", "crid": "crid_10", "h": 250, - "w": 300 - } + "w": 300, + "mtype": 0 + } ] } ], @@ -78,9 +79,10 @@ } } ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], "expectedMakeBidsErrors": [ { - "value": "Failed to find impression for ID: \"WRONG-IMPRESSION_ID\"", + "value": "Unsupported return type", "comparison": "literal" } ] diff --git a/adapters/xeworks/params_test.go b/adapters/xeworks/params_test.go new file mode 100644 index 00000000000..68d36096049 --- /dev/null +++ b/adapters/xeworks/params_test.go @@ -0,0 +1,53 @@ +package xeworks + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{"env":"xe-stage", "pid":"123456"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderXeworks, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected xeworks params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"some": "param"}`, + `{"env":"xe-stage"}`, + `{"pid":"1234"}`, + `{"othervalue":"Lorem ipsum"}`, + `{"env":"xe-stage", pid:""}`, + `{"env":"", pid:"1234"}`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderXeworks, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/xeworks/xeworks.go b/adapters/xeworks/xeworks.go new file mode 100644 index 00000000000..35e551b1034 --- /dev/null +++ b/adapters/xeworks/xeworks.go @@ -0,0 +1,161 @@ +package xeworks + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type bidType struct { + Type string `json:"type"` +} + +type bidExt struct { + Prebid bidType `json:"prebid"` +} + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + tmpl, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint URL template: %v", err) + } + + bidder := &adapter{ + endpoint: tmpl, + } + + return bidder, nil +} + +func (a *adapter) buildEndpointFromRequest(imp *openrtb2.Imp) (string, error) { + var impExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize bidder impression extension: %v", err), + } + } + + var xeworksExt openrtb_ext.ExtXeworks + if err := json.Unmarshal(impExt.Bidder, &xeworksExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize Xeworks extension: %v", err), + } + } + + endpointParams := macros.EndpointTemplateParams{ + Host: xeworksExt.Env, + SourceId: xeworksExt.Pid, + } + + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errs []error + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestCopy := *request + for _, imp := range request.Imp { + requestCopy.Imp = []openrtb2.Imp{imp} + + endpoint, err := a.buildEndpointFromRequest(&imp) + if err != nil { + errs = append(errs, err) + continue + } + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + request := &adapters.RequestData{ + Method: http.MethodPost, + Body: requestJSON, + Uri: endpoint, + Headers: headers, + } + + requests = append(requests, request) + } + + return requests, errs +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + if bidderRawResponse.StatusCode == http.StatusServiceUnavailable { + return nil, []error{&errortypes.BadInput{ + Message: "Bidder Xeworks is unavailable. Please contact the bidder support.", + }} + } + + if err := adapters.CheckResponseStatusCodeForErrors(bidderRawResponse); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(bidderRawResponse.Body, &bidResp); err != nil { + return nil, []error{err} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Array SeatBid cannot be empty", + }} + } + + return prepareBidResponse(bidResp.SeatBid) +} + +func prepareBidResponse(seats []openrtb2.SeatBid) (*adapters.BidderResponse, []error) { + errs := []error{} + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seats)) + + for _, seatBid := range seats { + for bidId, bid := range seatBid.Bid { + var bidExt bidExt + if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse Bid[%d].Ext: %s", bidId, err.Error()), + }) + continue + } + + bidType, err := openrtb_ext.ParseBidType(bidExt.Prebid.Type) + if err != nil { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bid[%d].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got '%s'", bidId, bidExt.Prebid.Type), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[bidId], + BidType: bidType, + }) + } + } + + return bidResponse, errs +} diff --git a/adapters/xeworks/xeworks_test.go b/adapters/xeworks/xeworks_test.go new file mode 100644 index 00000000000..4869a05a229 --- /dev/null +++ b/adapters/xeworks/xeworks_test.go @@ -0,0 +1,27 @@ +package xeworks + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderSmartHub, + config.Adapter{ + Endpoint: "http://prebid-srv.xe.works/?pid={{.SourceId}}&host={{.Host}}", + }, + config.Server{ + ExternalUrl: "http://hosturl.com", + GvlID: 1, + DataCenter: "2", + }, + ) + + assert.NoError(t, buildErr) + adapterstest.RunJSONBidderTest(t, "xeworkstest", bidder) +} diff --git a/adapters/xeworks/xeworkstest/exemplary/banner.json b/adapters/xeworks/xeworkstest/exemplary/banner.json new file mode 100644 index 00000000000..657c36c6d58 --- /dev/null +++ b/adapters/xeworks/xeworkstest/exemplary/banner.json @@ -0,0 +1,279 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + }, + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=533faf0754cd43ceab591077781b909b&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=533faf0754cd43ceab591077781b909b&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/xeworks/xeworkstest/exemplary/native.json b/adapters/xeworks/xeworkstest/exemplary/native.json new file mode 100644 index 00000000000..9c8eb6a1dbc --- /dev/null +++ b/adapters/xeworks/xeworkstest/exemplary/native.json @@ -0,0 +1,162 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=533faf0754cd43ceab591077781b909b&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/xeworks/xeworkstest/exemplary/video.json b/adapters/xeworks/xeworkstest/exemplary/video.json new file mode 100644 index 00000000000..5f509baad32 --- /dev/null +++ b/adapters/xeworks/xeworkstest/exemplary/video.json @@ -0,0 +1,202 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=533faf0754cd43ceab591077781b909b&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b909b" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/bad-response.json b/adapters/xeworks/xeworkstest/supplemental/bad-response.json new file mode 100644 index 00000000000..4f8b9bd2d79 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/bad-response.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=533faf0754cd43ceab591077781b902&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/empty-mediatype.json b/adapters/xeworks/xeworkstest/supplemental/empty-mediatype.json new file mode 100644 index 00000000000..c2d62d09c11 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/empty-mediatype.json @@ -0,0 +1,188 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=533faf0754cd43ceab591077781b902&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "533faf0754cd43ceab591077781b902" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "some": "value" + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid[1].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got ''", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/empty-seatbid-0-bid.json b/adapters/xeworks/xeworkstest/supplemental/empty-seatbid-0-bid.json new file mode 100644 index 00000000000..98bcbbeb8df --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/empty-seatbid-0-bid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/empty-seatbid.json b/adapters/xeworks/xeworkstest/supplemental/empty-seatbid.json new file mode 100644 index 00000000000..d522e8ce292 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/empty-seatbid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "bidid": "359da97d0384d8a14767029c18fd840d", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Array SeatBid cannot be empty", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/invalid-ext-bidder-object.json b/adapters/xeworks/xeworkstest/supplemental/invalid-ext-bidder-object.json new file mode 100644 index 00000000000..fe7fd9c8b19 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/invalid-ext-bidder-object.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": [] + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize Xeworks extension: json: cannot unmarshal array into Go value of type openrtb_ext.ExtXeworks", + "comparison": "literal" + } + ] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/invalid-ext-object.json b/adapters/xeworks/xeworkstest/supplemental/invalid-ext-object.json new file mode 100644 index 00000000000..aa215eb3e34 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/invalid-ext-object.json @@ -0,0 +1,47 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": "" + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize bidder impression extension: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/invalid-mediatype.json b/adapters/xeworks/xeworkstest/supplemental/invalid-mediatype.json new file mode 100644 index 00000000000..39e0cd236e0 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/invalid-mediatype.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "wrong" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bid[1].Ext.Prebid.Type expects one of the following values: 'banner', 'native', 'video', 'audio', got 'wrong'", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/status-204.json b/adapters/xeworks/xeworkstest/supplemental/status-204.json new file mode 100644 index 00000000000..72dbf94dfc3 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/status-204.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/status-400.json b/adapters/xeworks/xeworkstest/supplemental/status-400.json new file mode 100644 index 00000000000..220746084e6 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/status-400.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 400, + "body": "The Key has a different ad format" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/status-503.json b/adapters/xeworks/xeworkstest/supplemental/status-503.json new file mode 100644 index 00000000000..00b24dc97b0 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/status-503.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bidder Xeworks is unavailable. Please contact the bidder support.", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/xeworks/xeworkstest/supplemental/unexpected-status.json b/adapters/xeworks/xeworkstest/supplemental/unexpected-status.json new file mode 100644 index 00000000000..fd1b4f9d3f0 --- /dev/null +++ b/adapters/xeworks/xeworkstest/supplemental/unexpected-status.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid-srv.xe.works/?pid=dc230510f7eb516f0eb9a10e5913d3b5&host=xe-stage", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "xe-stage", + "pid": "dc230510f7eb516f0eb9a10e5913d3b5" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 403, + "body": "Access is denied" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/yahoossp/params_test.go b/adapters/yahooAds/params_test.go similarity index 76% rename from adapters/yahoossp/params_test.go rename to adapters/yahooAds/params_test.go index 47d8d26894b..c0deaaa32c9 100644 --- a/adapters/yahoossp/params_test.go +++ b/adapters/yahooAds/params_test.go @@ -1,4 +1,4 @@ -package yahoossp +package yahooAds import ( "encoding/json" @@ -7,11 +7,11 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -// This file actually intends to test static/bidder-params/yahoossp.json +// This file actually intends to test static/bidder-params/yahooAds.json // -// These also validate the format of the external API: request.imp[i].ext.yahoossp +// These also validate the format of the external API: request.imp[i].ext.yahooAds -// TestValidParams makes sure that the yahoossp schema accepts all imp.ext fields which we intend to support. +// TestValidParams makes sure that the yahooAds schema accepts all imp.ext fields which we intend to support. func TestValidParams(t *testing.T) { validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") if err != nil { @@ -19,13 +19,13 @@ func TestValidParams(t *testing.T) { } for _, validParam := range validParams { - if err := validator.Validate(openrtb_ext.BidderYahooSSP, json.RawMessage(validParam)); err != nil { - t.Errorf("Schema rejected yahoossp params: %s", validParam) + if err := validator.Validate(openrtb_ext.BidderYahooAds, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected yahooAds params: %s", validParam) } } } -// TestInvalidParams makes sure that the yahoossp schema rejects all the imp.ext fields we don't support. +// TestInvalidParams makes sure that the yahooAds schema rejects all the imp.ext fields we don't support. func TestInvalidParams(t *testing.T) { validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") if err != nil { @@ -33,7 +33,7 @@ func TestInvalidParams(t *testing.T) { } for _, invalidParam := range invalidParams { - if err := validator.Validate(openrtb_ext.BidderYahooSSP, json.RawMessage(invalidParam)); err == nil { + if err := validator.Validate(openrtb_ext.BidderYahooAds, json.RawMessage(invalidParam)); err == nil { t.Errorf("Schema allowed unexpected params: %s", invalidParam) } } diff --git a/adapters/yahoossp/yahoossp.go b/adapters/yahooAds/yahooAds.go similarity index 95% rename from adapters/yahoossp/yahoossp.go rename to adapters/yahooAds/yahooAds.go index 797ea708be6..3597d0e359c 100644 --- a/adapters/yahoossp/yahoossp.go +++ b/adapters/yahooAds/yahooAds.go @@ -1,4 +1,4 @@ -package yahoossp +package yahooAds import ( "encoding/json" @@ -41,8 +41,8 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E continue } - var yahoosspExt openrtb_ext.ExtImpYahooSSP - err = json.Unmarshal(bidderExt.Bidder, &yahoosspExt) + var yahooAdsExt openrtb_ext.ExtImpYahooAds + err = json.Unmarshal(bidderExt.Bidder, &yahooAdsExt) if err != nil { err = &errortypes.BadInput{ Message: fmt.Sprintf("imp #%d: %s", idx, err.Error()), @@ -64,7 +64,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E reqCopy.App = &appCopy } - if err := changeRequestForBidService(&reqCopy, &yahoosspExt); err != nil { + if err := changeRequestForBidService(&reqCopy, &yahooAdsExt); err != nil { errors = append(errors, err) continue } @@ -150,7 +150,7 @@ func getImpInfo(impId string, imps []openrtb2.Imp) (bool, openrtb_ext.BidType) { return exists, mediaType } -func changeRequestForBidService(request *openrtb2.BidRequest, extension *openrtb_ext.ExtImpYahooSSP) error { +func changeRequestForBidService(request *openrtb2.BidRequest, extension *openrtb_ext.ExtImpYahooAds) error { /* Always override the tag ID and (site ID or app ID) of the request */ request.Imp[0].TagID = extension.Pos if request.Site != nil { @@ -218,7 +218,7 @@ func validateBanner(banner *openrtb2.Banner) error { return nil } -// Builder builds a new instance of the YahooSSP adapter for the given bidder with the given config. +// Builder builds a new instance of the YahooAds adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &adapter{ URI: config.Endpoint, diff --git a/adapters/yahoossp/yahoossp_test.go b/adapters/yahooAds/yahooAds_test.go similarity index 64% rename from adapters/yahoossp/yahoossp_test.go rename to adapters/yahooAds/yahooAds_test.go index eeb739c612f..924eabd5ec1 100644 --- a/adapters/yahoossp/yahoossp_test.go +++ b/adapters/yahooAds/yahooAds_test.go @@ -1,4 +1,4 @@ -package yahoossp +package yahooAds import ( "testing" @@ -10,8 +10,8 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -func TestYahooSSPBidderEndpointConfig(t *testing.T) { - bidder, buildErr := Builder(openrtb_ext.BidderYahooSSP, config.Adapter{ +func TestYahooAdsBidderEndpointConfig(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderYahooAds, config.Adapter{ Endpoint: "http://localhost/bid", }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) @@ -19,17 +19,17 @@ func TestYahooSSPBidderEndpointConfig(t *testing.T) { t.Fatalf("Builder returned unexpected error %v", buildErr) } - bidderYahooSSP := bidder.(*adapter) + bidderYahooAds := bidder.(*adapter) - assert.Equal(t, "http://localhost/bid", bidderYahooSSP.URI) + assert.Equal(t, "http://localhost/bid", bidderYahooAds.URI) } func TestJsonSamples(t *testing.T) { - bidder, buildErr := Builder(openrtb_ext.BidderYahooSSP, config.Adapter{}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + bidder, buildErr := Builder(openrtb_ext.BidderYahooAds, config.Adapter{}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { t.Fatalf("Builder returned unexpected error %v", buildErr) } - adapterstest.RunJSONBidderTest(t, "yahoossptest", bidder) + adapterstest.RunJSONBidderTest(t, "yahooAdstest", bidder) } diff --git a/adapters/yahoossp/yahoossptest/exemplary/simple-app-banner.json b/adapters/yahooAds/yahooAdstest/exemplary/simple-app-banner.json similarity index 98% rename from adapters/yahoossp/yahoossptest/exemplary/simple-app-banner.json rename to adapters/yahooAds/yahooAdstest/exemplary/simple-app-banner.json index 319e22e31dd..7ad41161915 100644 --- a/adapters/yahoossp/yahoossptest/exemplary/simple-app-banner.json +++ b/adapters/yahooAds/yahooAdstest/exemplary/simple-app-banner.json @@ -77,7 +77,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yahoossp/yahoossptest/exemplary/simple-banner.json b/adapters/yahooAds/yahooAdstest/exemplary/simple-banner.json similarity index 98% rename from adapters/yahoossp/yahoossptest/exemplary/simple-banner.json rename to adapters/yahooAds/yahooAdstest/exemplary/simple-banner.json index 344bbe9af94..7036664d4ad 100644 --- a/adapters/yahoossp/yahoossptest/exemplary/simple-banner.json +++ b/adapters/yahooAds/yahooAdstest/exemplary/simple-banner.json @@ -77,7 +77,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yahoossp/yahoossptest/exemplary/simple-video.json b/adapters/yahooAds/yahooAdstest/exemplary/simple-video.json similarity index 98% rename from adapters/yahoossp/yahoossptest/exemplary/simple-video.json rename to adapters/yahooAds/yahooAdstest/exemplary/simple-video.json index d62272643a1..ebf7af93d53 100644 --- a/adapters/yahoossp/yahoossptest/exemplary/simple-video.json +++ b/adapters/yahooAds/yahooAdstest/exemplary/simple-video.json @@ -92,7 +92,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yahoossp/yahoossptest/supplemental/empty-banner-format.json b/adapters/yahooAds/yahooAdstest/supplemental/empty-banner-format.json similarity index 100% rename from adapters/yahoossp/yahoossptest/supplemental/empty-banner-format.json rename to adapters/yahooAds/yahooAdstest/supplemental/empty-banner-format.json diff --git a/adapters/yahoossp/yahoossptest/supplemental/invalid-banner-height.json b/adapters/yahooAds/yahooAdstest/supplemental/invalid-banner-height.json similarity index 100% rename from adapters/yahoossp/yahoossptest/supplemental/invalid-banner-height.json rename to adapters/yahooAds/yahooAdstest/supplemental/invalid-banner-height.json diff --git a/adapters/yahoossp/yahoossptest/supplemental/invalid-banner-width.json b/adapters/yahooAds/yahooAdstest/supplemental/invalid-banner-width.json similarity index 100% rename from adapters/yahoossp/yahoossptest/supplemental/invalid-banner-width.json rename to adapters/yahooAds/yahooAdstest/supplemental/invalid-banner-width.json diff --git a/adapters/yahoossp/yahoossptest/supplemental/non-supported-requests-bids-ignored.json b/adapters/yahooAds/yahooAdstest/supplemental/non-supported-requests-bids-ignored.json similarity index 98% rename from adapters/yahoossp/yahoossptest/supplemental/non-supported-requests-bids-ignored.json rename to adapters/yahooAds/yahooAdstest/supplemental/non-supported-requests-bids-ignored.json index 11a93f685a4..c0d77fa496b 100644 --- a/adapters/yahoossp/yahoossptest/supplemental/non-supported-requests-bids-ignored.json +++ b/adapters/yahooAds/yahooAdstest/supplemental/non-supported-requests-bids-ignored.json @@ -74,7 +74,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yahoossp/yahoossptest/supplemental/required-nobidder-info.json b/adapters/yahooAds/yahooAdstest/supplemental/required-nobidder-info.json similarity index 100% rename from adapters/yahoossp/yahoossptest/supplemental/required-nobidder-info.json rename to adapters/yahooAds/yahooAdstest/supplemental/required-nobidder-info.json diff --git a/adapters/yahoossp/yahoossptest/supplemental/server-error.json b/adapters/yahooAds/yahooAdstest/supplemental/server-error.json similarity index 100% rename from adapters/yahoossp/yahoossptest/supplemental/server-error.json rename to adapters/yahooAds/yahooAdstest/supplemental/server-error.json diff --git a/adapters/yahoossp/yahoossptest/supplemental/server-response-wrong-impid.json b/adapters/yahooAds/yahooAdstest/supplemental/server-response-wrong-impid.json similarity index 98% rename from adapters/yahoossp/yahoossptest/supplemental/server-response-wrong-impid.json rename to adapters/yahooAds/yahooAdstest/supplemental/server-response-wrong-impid.json index 5b33f3af67d..f40819497a8 100644 --- a/adapters/yahoossp/yahoossptest/supplemental/server-response-wrong-impid.json +++ b/adapters/yahooAds/yahooAdstest/supplemental/server-response-wrong-impid.json @@ -76,7 +76,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "wrong", diff --git a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-gpp-overwrite.json b/adapters/yahooAds/yahooAdstest/supplemental/simple-banner-gpp-overwrite.json similarity index 98% rename from adapters/yahoossp/yahoossptest/supplemental/simple-banner-gpp-overwrite.json rename to adapters/yahooAds/yahooAdstest/supplemental/simple-banner-gpp-overwrite.json index ffc7ae651e7..94c895b996d 100644 --- a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-gpp-overwrite.json +++ b/adapters/yahooAds/yahooAdstest/supplemental/simple-banner-gpp-overwrite.json @@ -92,7 +92,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-gpp.json b/adapters/yahooAds/yahooAdstest/supplemental/simple-banner-gpp.json similarity index 98% rename from adapters/yahoossp/yahoossptest/supplemental/simple-banner-gpp.json rename to adapters/yahooAds/yahooAdstest/supplemental/simple-banner-gpp.json index 88f97d116d3..3d5aff6c531 100644 --- a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-gpp.json +++ b/adapters/yahooAds/yahooAdstest/supplemental/simple-banner-gpp.json @@ -87,7 +87,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-ignore-width-when-height-missing.json b/adapters/yahooAds/yahooAdstest/supplemental/simple-banner-ignore-width-when-height-missing.json similarity index 98% rename from adapters/yahoossp/yahoossptest/supplemental/simple-banner-ignore-width-when-height-missing.json rename to adapters/yahooAds/yahooAdstest/supplemental/simple-banner-ignore-width-when-height-missing.json index 16e64c6165b..1206005970c 100644 --- a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-ignore-width-when-height-missing.json +++ b/adapters/yahooAds/yahooAdstest/supplemental/simple-banner-ignore-width-when-height-missing.json @@ -78,7 +78,7 @@ "id": "test-request-id", "seatbid": [ { - "seat": "yahoossp", + "seat": "yahooAds", "bid": [{ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", diff --git a/adapters/yeahmobi/yeahmobi.go b/adapters/yeahmobi/yeahmobi.go index 3079225876d..d25b2eab541 100644 --- a/adapters/yeahmobi/yeahmobi.go +++ b/adapters/yeahmobi/yeahmobi.go @@ -7,7 +7,6 @@ import ( "net/url" "text/template" - "github.com/golang/glog" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" @@ -50,7 +49,6 @@ func (adapter *YeahmobiAdapter) makeRequest(request *openrtb2.BidRequest) (*adap yeahmobiExt, errs := getYeahmobiExt(request) if yeahmobiExt == nil { - glog.Fatal("Invalid ExtImpYeahmobi value") return nil, errs } endPoint, err := adapter.getEndpoint(yeahmobiExt) diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/banner.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/banner.json new file mode 100644 index 00000000000..cdfaf37e45d --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/banner.json @@ -0,0 +1,146 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://whatever.url", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 1.234, + "adm": "", + "adomain": [ + "some-adomain" + ], + "cid": "1234", + "crid": "2345", + "h": 1, + "w": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "zeta_global_ssp" + } + ], + "bidid": "some-bidid", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "price": 1.234, + "adm": "", + "adomain": [ + "some-adomain" + ], + "cid": "1234", + "crid": "2345", + "h": 1, + "w": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/no-bid.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/no-bid.json new file mode 100644 index 00000000000..68aabbed257 --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/no-bid.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://whatever.url", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/video.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/video.json new file mode 100644 index 00000000000..248f4b0487a --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/exemplary/video.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "ext": { + "bidder": {} + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "ext": { + "bidder": {} + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "w": 1200, + "h": 900 + }, + "test": 1 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/bad-request.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/bad-request.json new file mode 100644 index 00000000000..38f3bd326d0 --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/bad-request.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://whatever.url", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/invalid-bid-type.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/invalid-bid-type.json new file mode 100644 index 00000000000..4a55670720d --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/invalid-bid-type.json @@ -0,0 +1,125 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://whatever.url", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 1.234, + "adm": "", + "adomain": [ + "some-adomain" + ], + "cid": "1234", + "crid": "2345", + "h": 1, + "w": 1, + "ext": { + "prebid": { + "type": "invalid" + } + } + } + ], + "seat": "zeta_global_ssp" + } + ], + "bidid": "some-bidid", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "invalid BidType: invalid", + "comparison": "literal" + } + ] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/no-bid-type.json.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/no-bid-type.json.json new file mode 100644 index 00000000000..1992586435f --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/no-bid-type.json.json @@ -0,0 +1,120 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://whatever.url", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 1.234, + "adm": "", + "adomain": [ + "some-adomain" + ], + "cid": "1234", + "crid": "2345", + "h": 1, + "w": 1 + } + ], + "seat": "zeta_global_ssp" + } + ], + "bidid": "some-bidid", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Failed to parse impression \"some-impression-id\" mediatype", + "comparison": "literal" + } + ] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/server-error.json b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/server-error.json new file mode 100644 index 00000000000..037c7307889 --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp-test/supplemental/server-error.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://whatever.url", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + } + }, + "mockResponse": { + "status": 500 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp.go b/adapters/zeta_global_ssp/zeta_global_ssp.go new file mode 100644 index 00000000000..7a5f3395724 --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp.go @@ -0,0 +1,92 @@ +package zeta_global_ssp + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + requestJson, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJson, + Headers: headers, + } + + return []*adapters.RequestData{requestData}, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errors []error + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForBid(seatBid.Bid[i]) + if err != nil { + errors = append(errors, err) + continue + } + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, errors +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse impression \"%s\" mediatype", bid.ImpID), + } +} diff --git a/adapters/zeta_global_ssp/zeta_global_ssp_test.go b/adapters/zeta_global_ssp/zeta_global_ssp_test.go new file mode 100644 index 00000000000..3b7be288fa1 --- /dev/null +++ b/adapters/zeta_global_ssp/zeta_global_ssp_test.go @@ -0,0 +1,21 @@ +package zeta_global_ssp + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderZetaGlobalSsp, config.Adapter{ + Endpoint: "http://whatever.url"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "zeta_global_ssp-test", bidder) +} diff --git a/analytics/config/config_test.go b/analytics/config/config_test.go index 996a989e697..c0ad9c26a16 100644 --- a/analytics/config/config_test.go +++ b/analytics/config/config_test.go @@ -20,7 +20,6 @@ func TestSampleModule(t *testing.T) { am.LogAuctionObject(&analytics.AuctionObject{ Status: http.StatusOK, Errors: nil, - Request: &openrtb2.BidRequest{}, Response: &openrtb2.BidResponse{}, }) if count != 1 { diff --git a/analytics/core.go b/analytics/core.go index d5d5e9146f7..eca93741bd2 100644 --- a/analytics/core.go +++ b/analytics/core.go @@ -9,14 +9,9 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -/* - PBSAnalyticsModule must be implemented by any analytics module that does transactional logging. - - New modules can use the /analytics/endpoint_data_objects, extract the - information required and are responsible for handling all their logging activities inside LogAuctionObject, LogAmpObject - LogCookieSyncObject and LogSetUIDObject method implementations. -*/ - +// PBSAnalyticsModule must be implemented by analytics modules to extract the required information and logging +// activities. Do not use marshal the parameter objects directly as they can change over time. Use a separate +// model for each analytics module and transform as appropriate. type PBSAnalyticsModule interface { LogAuctionObject(*AuctionObject) LogVideoObject(*VideoObject) @@ -30,34 +25,37 @@ type PBSAnalyticsModule interface { type AuctionObject struct { Status int Errors []error - Request *openrtb2.BidRequest Response *openrtb2.BidResponse Account *config.Account StartTime time.Time HookExecutionOutcome []hookexecution.StageOutcome + SeatNonBid []openrtb_ext.SeatNonBid + RequestWrapper *openrtb_ext.RequestWrapper } // Loggable object of a transaction at /openrtb2/amp endpoint type AmpObject struct { Status int Errors []error - Request *openrtb2.BidRequest AuctionResponse *openrtb2.BidResponse AmpTargetingValues map[string]string Origin string StartTime time.Time HookExecutionOutcome []hookexecution.StageOutcome + SeatNonBid []openrtb_ext.SeatNonBid + RequestWrapper *openrtb_ext.RequestWrapper } // Loggable object of a transaction at /openrtb2/video endpoint type VideoObject struct { - Status int - Errors []error - Request *openrtb2.BidRequest - Response *openrtb2.BidResponse - VideoRequest *openrtb_ext.BidRequestVideo - VideoResponse *openrtb_ext.BidResponseVideo - StartTime time.Time + Status int + Errors []error + Response *openrtb2.BidResponse + VideoRequest *openrtb_ext.BidRequestVideo + VideoResponse *openrtb_ext.BidResponseVideo + StartTime time.Time + SeatNonBid []openrtb_ext.SeatNonBid + RequestWrapper *openrtb_ext.RequestWrapper } // Loggable object of a transaction at /setuid @@ -88,7 +86,7 @@ type UsersyncInfo struct { SupportCORS bool `json:"supportCORS,omitempty"` } -// NotificationEvent is a loggable object +// NotificationEvent object of a transaction at /event type NotificationEvent struct { Request *EventRequest `json:"request"` Account *config.Account `json:"account"` diff --git a/analytics/filesystem/file_module.go b/analytics/filesystem/file_module.go index d6a03ea2d8a..9a357529c3a 100644 --- a/analytics/filesystem/file_module.go +++ b/analytics/filesystem/file_module.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/chasex/glog" + "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/analytics" ) @@ -27,7 +28,6 @@ type FileLogger struct { // Writes AuctionObject to file func (f *FileLogger) LogAuctionObject(ao *analytics.AuctionObject) { - //Code to parse the object and log in a way required var b bytes.Buffer b.WriteString(jsonifyAuctionObject(ao)) f.Logger.Debug(b.String()) @@ -103,13 +103,29 @@ func NewFileLogger(filename string) (analytics.PBSAnalyticsModule, error) { } func jsonifyAuctionObject(ao *analytics.AuctionObject) string { - type alias analytics.AuctionObject + var logEntry *logAuction + if ao != nil { + var request *openrtb2.BidRequest + if ao.RequestWrapper != nil { + request = ao.RequestWrapper.BidRequest + } + logEntry = &logAuction{ + Status: ao.Status, + Errors: ao.Errors, + Request: request, + Response: ao.Response, + Account: ao.Account, + StartTime: ao.StartTime, + HookExecutionOutcome: ao.HookExecutionOutcome, + } + } + b, err := json.Marshal(&struct { Type RequestType `json:"type"` - *alias + *logAuction }{ - Type: AUCTION, - alias: (*alias)(ao), + Type: AUCTION, + logAuction: logEntry, }) if err == nil { @@ -120,13 +136,29 @@ func jsonifyAuctionObject(ao *analytics.AuctionObject) string { } func jsonifyVideoObject(vo *analytics.VideoObject) string { - type alias analytics.VideoObject + var logEntry *logVideo + if vo != nil { + var request *openrtb2.BidRequest + if vo.RequestWrapper != nil { + request = vo.RequestWrapper.BidRequest + } + logEntry = &logVideo{ + Status: vo.Status, + Errors: vo.Errors, + Request: request, + Response: vo.Response, + VideoRequest: vo.VideoRequest, + VideoResponse: vo.VideoResponse, + StartTime: vo.StartTime, + } + } + b, err := json.Marshal(&struct { Type RequestType `json:"type"` - *alias + *logVideo }{ - Type: VIDEO, - alias: (*alias)(vo), + Type: VIDEO, + logVideo: logEntry, }) if err == nil { @@ -137,14 +169,21 @@ func jsonifyVideoObject(vo *analytics.VideoObject) string { } func jsonifyCookieSync(cso *analytics.CookieSyncObject) string { - type alias analytics.CookieSyncObject + var logEntry *logUserSync + if cso != nil { + logEntry = &logUserSync{ + Status: cso.Status, + Errors: cso.Errors, + BidderStatus: cso.BidderStatus, + } + } b, err := json.Marshal(&struct { Type RequestType `json:"type"` - *alias + *logUserSync }{ - Type: COOKIE_SYNC, - alias: (*alias)(cso), + Type: COOKIE_SYNC, + logUserSync: logEntry, }) if err == nil { @@ -155,13 +194,23 @@ func jsonifyCookieSync(cso *analytics.CookieSyncObject) string { } func jsonifySetUIDObject(so *analytics.SetUIDObject) string { - type alias analytics.SetUIDObject + var logEntry *logSetUID + if so != nil { + logEntry = &logSetUID{ + Status: so.Status, + Bidder: so.Bidder, + UID: so.UID, + Errors: so.Errors, + Success: so.Success, + } + } + b, err := json.Marshal(&struct { Type RequestType `json:"type"` - *alias + *logSetUID }{ - Type: SETUID, - alias: (*alias)(so), + Type: SETUID, + logSetUID: logEntry, }) if err == nil { @@ -172,13 +221,30 @@ func jsonifySetUIDObject(so *analytics.SetUIDObject) string { } func jsonifyAmpObject(ao *analytics.AmpObject) string { - type alias analytics.AmpObject + var logEntry *logAMP + if ao != nil { + var request *openrtb2.BidRequest + if ao.RequestWrapper != nil { + request = ao.RequestWrapper.BidRequest + } + logEntry = &logAMP{ + Status: ao.Status, + Errors: ao.Errors, + Request: request, + AuctionResponse: ao.AuctionResponse, + AmpTargetingValues: ao.AmpTargetingValues, + Origin: ao.Origin, + StartTime: ao.StartTime, + HookExecutionOutcome: ao.HookExecutionOutcome, + } + } + b, err := json.Marshal(&struct { Type RequestType `json:"type"` - *alias + *logAMP }{ - Type: AMP, - alias: (*alias)(ao), + Type: AMP, + logAMP: logEntry, }) if err == nil { @@ -189,13 +255,20 @@ func jsonifyAmpObject(ao *analytics.AmpObject) string { } func jsonifyNotificationEventObject(ne *analytics.NotificationEvent) string { - type alias analytics.NotificationEvent + var logEntry *logNotificationEvent + if ne != nil { + logEntry = &logNotificationEvent{ + Request: ne.Request, + Account: ne.Account, + } + } + b, err := json.Marshal(&struct { Type RequestType `json:"type"` - *alias + *logNotificationEvent }{ - Type: NOTIFICATION_EVENT, - alias: (*alias)(ne), + Type: NOTIFICATION_EVENT, + logNotificationEvent: logEntry, }) if err == nil { diff --git a/analytics/filesystem/model.go b/analytics/filesystem/model.go new file mode 100644 index 00000000000..9fc7a6e19a2 --- /dev/null +++ b/analytics/filesystem/model.go @@ -0,0 +1,61 @@ +package filesystem + +import ( + "time" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/hooks/hookexecution" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type logAuction struct { + Status int + Errors []error + Request *openrtb2.BidRequest + Response *openrtb2.BidResponse + Account *config.Account + StartTime time.Time + HookExecutionOutcome []hookexecution.StageOutcome +} + +type logVideo struct { + Status int + Errors []error + Request *openrtb2.BidRequest + Response *openrtb2.BidResponse + VideoRequest *openrtb_ext.BidRequestVideo + VideoResponse *openrtb_ext.BidResponseVideo + StartTime time.Time +} + +type logSetUID struct { + Status int + Bidder string + UID string + Errors []error + Success bool +} + +type logUserSync struct { + Status int + Errors []error + BidderStatus []*analytics.CookieSyncBidder +} + +type logAMP struct { + Status int + Errors []error + Request *openrtb2.BidRequest + AuctionResponse *openrtb2.BidResponse + AmpTargetingValues map[string]string + Origin string + StartTime time.Time + HookExecutionOutcome []hookexecution.StageOutcome +} + +type logNotificationEvent struct { + Request *analytics.EventRequest `json:"request"` + Account *config.Account `json:"account"` +} diff --git a/analytics/pubstack/helpers/json.go b/analytics/pubstack/helpers/json.go index f02f1120626..368c79e3f6a 100644 --- a/analytics/pubstack/helpers/json.go +++ b/analytics/pubstack/helpers/json.go @@ -4,16 +4,34 @@ import ( "encoding/json" "fmt" + "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/analytics" ) func JsonifyAuctionObject(ao *analytics.AuctionObject, scope string) ([]byte, error) { + var logEntry *logAuction + if ao != nil { + var request *openrtb2.BidRequest + if ao.RequestWrapper != nil { + request = ao.RequestWrapper.BidRequest + } + logEntry = &logAuction{ + Status: ao.Status, + Errors: ao.Errors, + Request: request, + Response: ao.Response, + Account: ao.Account, + StartTime: ao.StartTime, + HookExecutionOutcome: ao.HookExecutionOutcome, + } + } + b, err := json.Marshal(&struct { Scope string `json:"scope"` - *analytics.AuctionObject + *logAuction }{ - Scope: scope, - AuctionObject: ao, + Scope: scope, + logAuction: logEntry, }) if err == nil { @@ -24,12 +42,29 @@ func JsonifyAuctionObject(ao *analytics.AuctionObject, scope string) ([]byte, er } func JsonifyVideoObject(vo *analytics.VideoObject, scope string) ([]byte, error) { + var logEntry *logVideo + if vo != nil { + var request *openrtb2.BidRequest + if vo.RequestWrapper != nil { + request = vo.RequestWrapper.BidRequest + } + logEntry = &logVideo{ + Status: vo.Status, + Errors: vo.Errors, + Request: request, + Response: vo.Response, + VideoRequest: vo.VideoRequest, + VideoResponse: vo.VideoResponse, + StartTime: vo.StartTime, + } + } + b, err := json.Marshal(&struct { Scope string `json:"scope"` - *analytics.VideoObject + *logVideo }{ - Scope: scope, - VideoObject: vo, + Scope: scope, + logVideo: logEntry, }) if err == nil { @@ -40,12 +75,21 @@ func JsonifyVideoObject(vo *analytics.VideoObject, scope string) ([]byte, error) } func JsonifyCookieSync(cso *analytics.CookieSyncObject, scope string) ([]byte, error) { + var logEntry *logUserSync + if cso != nil { + logEntry = &logUserSync{ + Status: cso.Status, + Errors: cso.Errors, + BidderStatus: cso.BidderStatus, + } + } + b, err := json.Marshal(&struct { Scope string `json:"scope"` - *analytics.CookieSyncObject + *logUserSync }{ - Scope: scope, - CookieSyncObject: cso, + Scope: scope, + logUserSync: logEntry, }) if err == nil { @@ -56,12 +100,23 @@ func JsonifyCookieSync(cso *analytics.CookieSyncObject, scope string) ([]byte, e } func JsonifySetUIDObject(so *analytics.SetUIDObject, scope string) ([]byte, error) { + var logEntry *logSetUID + if so != nil { + logEntry = &logSetUID{ + Status: so.Status, + Bidder: so.Bidder, + UID: so.UID, + Errors: so.Errors, + Success: so.Success, + } + } + b, err := json.Marshal(&struct { Scope string `json:"scope"` - *analytics.SetUIDObject + *logSetUID }{ - Scope: scope, - SetUIDObject: so, + Scope: scope, + logSetUID: logEntry, }) if err == nil { @@ -72,12 +127,30 @@ func JsonifySetUIDObject(so *analytics.SetUIDObject, scope string) ([]byte, erro } func JsonifyAmpObject(ao *analytics.AmpObject, scope string) ([]byte, error) { + var logEntry *logAMP + if ao != nil { + var request *openrtb2.BidRequest + if ao.RequestWrapper != nil { + request = ao.RequestWrapper.BidRequest + } + logEntry = &logAMP{ + Status: ao.Status, + Errors: ao.Errors, + Request: request, + AuctionResponse: ao.AuctionResponse, + AmpTargetingValues: ao.AmpTargetingValues, + Origin: ao.Origin, + StartTime: ao.StartTime, + HookExecutionOutcome: ao.HookExecutionOutcome, + } + } + b, err := json.Marshal(&struct { Scope string `json:"scope"` - *analytics.AmpObject + *logAMP }{ - Scope: scope, - AmpObject: ao, + Scope: scope, + logAMP: logEntry, }) if err == nil { diff --git a/analytics/pubstack/helpers/model.go b/analytics/pubstack/helpers/model.go new file mode 100644 index 00000000000..91b86d7fc86 --- /dev/null +++ b/analytics/pubstack/helpers/model.go @@ -0,0 +1,56 @@ +package helpers + +import ( + "time" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/hooks/hookexecution" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type logAuction struct { + Status int + Errors []error + Request *openrtb2.BidRequest + Response *openrtb2.BidResponse + Account *config.Account + StartTime time.Time + HookExecutionOutcome []hookexecution.StageOutcome +} + +type logVideo struct { + Status int + Errors []error + Request *openrtb2.BidRequest + Response *openrtb2.BidResponse + VideoRequest *openrtb_ext.BidRequestVideo + VideoResponse *openrtb_ext.BidResponseVideo + StartTime time.Time +} + +type logSetUID struct { + Status int + Bidder string + UID string + Errors []error + Success bool +} + +type logUserSync struct { + Status int + Errors []error + BidderStatus []*analytics.CookieSyncBidder +} + +type logAMP struct { + Status int + Errors []error + Request *openrtb2.BidRequest + AuctionResponse *openrtb2.BidResponse + AmpTargetingValues map[string]string + Origin string + StartTime time.Time + HookExecutionOutcome []hookexecution.StageOutcome +} diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go index 67598360419..504a1cfe17e 100644 --- a/analytics/pubstack/pubstack_module_test.go +++ b/analytics/pubstack/pubstack_module_test.go @@ -9,6 +9,7 @@ import ( "github.com/benbjohnson/clock" "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) @@ -86,6 +87,26 @@ func TestNewModuleSuccess(t *testing.T) { module.LogSetUIDObject(&analytics.SetUIDObject{Status: http.StatusOK}) }, }, + { + description: "Ignore excluded fields from marshal", + feature: auction, + logObject: func(module analytics.PBSAnalyticsModule) { + module.LogAuctionObject(&analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{}, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "123", + StatusCode: 34, + Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + }, + }, + }, + }, + }) + }, + }, } for _, tt := range tests { diff --git a/bidadjustment/apply.go b/bidadjustment/apply.go new file mode 100644 index 00000000000..4fa3b737b16 --- /dev/null +++ b/bidadjustment/apply.go @@ -0,0 +1,94 @@ +package bidadjustment + +import ( + "math" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + AdjustmentTypeCPM = "cpm" + AdjustmentTypeMultiplier = "multiplier" + AdjustmentTypeStatic = "static" + WildCard = "*" + Delimiter = "|" +) + +const maxNumOfCombos = 8 +const pricePrecision float64 = 10000 // Rounds to 4 Decimal Places +const minBid = 0.1 + +// Apply gets the highest priority adjustment slice given a map of rules, and applies those adjustments to a bid's price +func Apply(rules map[string][]openrtb_ext.Adjustment, bidInfo *adapters.TypedBid, bidderName openrtb_ext.BidderName, currency string, reqInfo *adapters.ExtraRequestInfo, bidType string) (float64, string) { + adjustments := []openrtb_ext.Adjustment{} + if len(rules) > 0 { + adjustments = get(rules, bidType, string(bidderName), bidInfo.Bid.DealID) + } else { + return bidInfo.Bid.Price, currency + } + adjustedPrice, adjustedCurrency := apply(adjustments, bidInfo.Bid.Price, currency, reqInfo) + + if bidInfo.Bid.DealID != "" && adjustedPrice < 0 { + return 0, currency + } + if bidInfo.Bid.DealID == "" && adjustedPrice <= 0 { + return minBid, currency + } + return adjustedPrice, adjustedCurrency +} + +func apply(adjustments []openrtb_ext.Adjustment, bidPrice float64, currency string, reqInfo *adapters.ExtraRequestInfo) (float64, string) { + if len(adjustments) == 0 { + return bidPrice, currency + } + originalBidPrice := bidPrice + + for _, adjustment := range adjustments { + switch adjustment.Type { + case AdjustmentTypeMultiplier: + bidPrice = bidPrice * adjustment.Value + case AdjustmentTypeCPM: + convertedVal, err := reqInfo.ConvertCurrency(adjustment.Value, adjustment.Currency, currency) + if err != nil { + return originalBidPrice, currency + } + bidPrice = bidPrice - convertedVal + case AdjustmentTypeStatic: + bidPrice = adjustment.Value + currency = adjustment.Currency + } + } + roundedBidPrice := math.Round(bidPrice*pricePrecision) / pricePrecision + + return roundedBidPrice, currency +} + +// get() should return the highest priority slice of adjustments from the map that we can match with the given bid info +// given the bid info, we create the same format of combinations that's present in the key of the ruleToAdjustments map +// the slice is ordered by priority from highest to lowest, as soon as we find a match, we return that slice +func get(rules map[string][]openrtb_ext.Adjustment, bidType, bidderName, dealID string) []openrtb_ext.Adjustment { + priorityRules := [maxNumOfCombos]string{} + if dealID != "" { + priorityRules[0] = bidType + Delimiter + bidderName + Delimiter + dealID + priorityRules[1] = bidType + Delimiter + bidderName + Delimiter + WildCard + priorityRules[2] = bidType + Delimiter + WildCard + Delimiter + dealID + priorityRules[3] = WildCard + Delimiter + bidderName + Delimiter + dealID + priorityRules[4] = bidType + Delimiter + WildCard + Delimiter + WildCard + priorityRules[5] = WildCard + Delimiter + bidderName + Delimiter + WildCard + priorityRules[6] = WildCard + Delimiter + WildCard + Delimiter + dealID + priorityRules[7] = WildCard + Delimiter + WildCard + Delimiter + WildCard + } else { + priorityRules[0] = bidType + Delimiter + bidderName + Delimiter + WildCard + priorityRules[1] = bidType + Delimiter + WildCard + Delimiter + WildCard + priorityRules[2] = WildCard + Delimiter + bidderName + Delimiter + WildCard + priorityRules[3] = WildCard + Delimiter + WildCard + Delimiter + WildCard + } + + for _, rule := range priorityRules { + if _, ok := rules[rule]; ok { + return rules[rule] + } + } + return nil +} diff --git a/bidadjustment/apply_test.go b/bidadjustment/apply_test.go new file mode 100644 index 00000000000..c0eb74ab419 --- /dev/null +++ b/bidadjustment/apply_test.go @@ -0,0 +1,532 @@ +package bidadjustment + +import ( + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetAndApply(t *testing.T) { + var ( + adjCur string = "EUR" + bidCur string = "USA" + ) + + testCases := []struct { + name string + givenRuleToAdjustments map[string][]openrtb_ext.Adjustment + givenBidderName openrtb_ext.BidderName + givenBidInfo *adapters.TypedBid + givenBidType string + setMock func(m *mock.Mock) + expectedBidPrice float64 + expectedCurrency string + }{ + { + name: "CpmAdjustment", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 10.0, + DealID: "dealId", + }, + }, + givenBidType: string(openrtb_ext.BidTypeBanner), + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 1.0, + Currency: adjCur, + }, + }, + "banner|bidderA|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + }, + }, + givenBidderName: "bidderA", + setMock: func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) }, + expectedBidPrice: 7.5, + expectedCurrency: bidCur, + }, + { + name: "StaticAdjustment", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 10.0, + DealID: "dealId", + }, + }, + givenBidType: string(openrtb_ext.BidTypeBanner), + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|bidderA|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 1.0, + Currency: adjCur, + }, + }, + "banner|bidderA|*": { + { + Type: AdjustmentTypeStatic, + Value: 2.0, + Currency: adjCur, + }, + }, + }, + givenBidderName: "bidderA", + setMock: nil, + expectedBidPrice: 2.0, + expectedCurrency: adjCur, + }, + { + name: "MultiplierAdjustment", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 10.0, + DealID: "dealId", + }, + }, + givenBidType: VideoInstream, + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|*|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 1.0, + Currency: adjCur, + }, + }, + "video-instream|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + }, + }, + givenBidderName: "bidderA", + setMock: nil, + expectedBidPrice: 20.0, + expectedCurrency: bidCur, + }, + { + name: "CpmAndMultiplierAdjustments", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 10.0, + DealID: "dealId", + }, + }, + givenBidType: VideoInstream, + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "video-instream|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + { + Type: AdjustmentTypeCPM, + Value: 1.0, + Currency: adjCur, + }, + }, + }, + givenBidderName: "bidderA", + setMock: func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) }, + expectedBidPrice: 17.5, + expectedCurrency: bidCur, + }, + { + name: "DealIdPresentAndNegativeAdjustedPrice", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 1.0, + DealID: "dealId", + }, + }, + givenBidType: VideoInstream, + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|*|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 1.0, + Currency: adjCur, + }, + }, + }, + givenBidderName: "bidderA", + setMock: func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) }, + expectedBidPrice: 0.0, + expectedCurrency: bidCur, + }, + { + name: "NoDealIdNegativeAdjustedPrice", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 1.0, + DealID: "", + }, + }, + givenBidType: string(openrtb_ext.BidTypeAudio), + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|*|*": { + { + Type: AdjustmentTypeCPM, + Value: 1.0, + Currency: adjCur, + }, + }, + }, + givenBidderName: "bidderA", + setMock: func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) }, + expectedBidPrice: 0.1, + expectedCurrency: bidCur, + }, + { + name: "NilMap", + givenBidInfo: &adapters.TypedBid{ + Bid: &openrtb2.Bid{ + Price: 10.0, + DealID: "dealId", + }, + }, + givenBidType: string(openrtb_ext.BidTypeBanner), + givenRuleToAdjustments: nil, + givenBidderName: "bidderA", + setMock: nil, + expectedBidPrice: 10.0, + expectedCurrency: bidCur, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + reqInfo := adapters.ExtraRequestInfo{} + if test.setMock != nil { + mockConversions := &mockConversions{} + test.setMock(&mockConversions.Mock) + reqInfo = adapters.NewExtraRequestInfo(mockConversions) + } + bidPrice, currencyAfterAdjustment := Apply(test.givenRuleToAdjustments, test.givenBidInfo, test.givenBidderName, bidCur, &reqInfo, test.givenBidType) + assert.Equal(t, test.expectedBidPrice, bidPrice) + assert.Equal(t, test.expectedCurrency, currencyAfterAdjustment) + }) + } +} + +type mockConversions struct { + mock.Mock +} + +func (m *mockConversions) GetRate(from string, to string) (float64, error) { + args := m.Called(from, to) + return args.Get(0).(float64), args.Error(1) +} + +func (m *mockConversions) GetRates() *map[string]map[string]float64 { + args := m.Called() + return args.Get(0).(*map[string]map[string]float64) +} + +func TestApply(t *testing.T) { + var ( + adjCur string = "EUR" + bidCur string = "USA" + ) + + testCases := []struct { + name string + givenAdjustments []openrtb_ext.Adjustment + setMock func(m *mock.Mock) + givenBidPrice float64 + expectedBidPrice float64 + expectedCurrency string + }{ + { + name: "CpmAdjustment", + givenAdjustments: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 1.0, Currency: adjCur}}, + givenBidPrice: 10.58687, + setMock: func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) }, + expectedBidPrice: 8.0869, + expectedCurrency: bidCur, + }, + { + name: "StaticAdjustment", + givenAdjustments: []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 4.0, Currency: adjCur}}, + givenBidPrice: 10.0, + setMock: nil, + expectedBidPrice: 4.0, + expectedCurrency: adjCur, + }, + { + name: "MultiplierAdjustment", + givenAdjustments: []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 3.0}}, + givenBidPrice: 10.0, + setMock: nil, + expectedBidPrice: 30.0, + expectedCurrency: bidCur, + }, + { + name: "NilAdjustment", + givenAdjustments: nil, + givenBidPrice: 10.0, + setMock: nil, + expectedBidPrice: 10.0, + expectedCurrency: bidCur, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + reqInfo := adapters.ExtraRequestInfo{} + if test.setMock != nil { + mockConversions := &mockConversions{} + test.setMock(&mockConversions.Mock) + reqInfo = adapters.NewExtraRequestInfo(mockConversions) + } + + bidPrice, currencyAfterAdjustment := apply(test.givenAdjustments, test.givenBidPrice, bidCur, &reqInfo) + assert.Equal(t, test.expectedBidPrice, bidPrice) + assert.Equal(t, test.expectedCurrency, currencyAfterAdjustment) + }) + } +} + +func TestGet(t *testing.T) { + testCases := []struct { + name string + givenRuleToAdjustments map[string][]openrtb_ext.Adjustment + givenBidType openrtb_ext.BidType + givenBidderName openrtb_ext.BidderName + givenDealId string + expected []openrtb_ext.Adjustment + }{ + { + name: "Priority1", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + "banner|bidderA|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + { + name: "Priority2", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|*": { + { + Type: AdjustmentTypeStatic, + Value: 5.0, + }, + }, + "banner|*|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 5.0}}, + }, + { + name: "Priority3", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|*|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 3.0, + Currency: "USD", + }, + }, + "*|bidderA|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}}, + }, + { + name: "Priority4", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|bidderA|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 3.0, + Currency: "USD", + }, + }, + "banner|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}}, + }, + { + name: "Priority5", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|*|*": { + { + Type: AdjustmentTypeCPM, + Value: 3.0, + Currency: "USD", + }, + }, + "*|bidderA|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}}, + }, + { + name: "Priority6", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|bidderA|*": { + { + Type: AdjustmentTypeCPM, + Value: 3.0, + Currency: "USD", + }, + }, + "*|*|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}}, + }, + { + name: "Priority7", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|*|dealId": { + { + Type: AdjustmentTypeCPM, + Value: 3.0, + Currency: "USD", + }, + }, + "*|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}}, + }, + { + name: "Priority8", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "*|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + "banner|bidderA|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderB", + givenDealId: "dealId", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + { + name: "NoDealID", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + "banner|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeBanner, + givenBidderName: "bidderA", + givenDealId: "", + expected: []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + { + name: "NoPriorityRulesMatch", + givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + "banner|bidderA|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeVideo, + givenBidderName: "bidderB", + givenDealId: "diffDealId", + expected: nil, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + adjArray := get(test.givenRuleToAdjustments, string(test.givenBidType), string(test.givenBidderName), test.givenDealId) + assert.Equal(t, test.expected, adjArray) + }) + } +} diff --git a/bidadjustment/build_rules.go b/bidadjustment/build_rules.go new file mode 100644 index 00000000000..bccb3bc86cf --- /dev/null +++ b/bidadjustment/build_rules.go @@ -0,0 +1,103 @@ +package bidadjustment + +import ( + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + VideoInstream = "video-instream" + VideoOutstream = "video-outstream" +) + +// BuildRules() will populate the rules map with a rule that's a combination of the mediaType, bidderName, and dealId for a particular adjustment +// The result will be a map that'll map a given rule with its adjustment +func BuildRules(bidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments) map[string][]openrtb_ext.Adjustment { + if bidAdjustments == nil { + return nil + } + rules := make(map[string][]openrtb_ext.Adjustment) + + buildRulesForMediaType(string(openrtb_ext.BidTypeBanner), bidAdjustments.MediaType.Banner, rules) + buildRulesForMediaType(string(openrtb_ext.BidTypeAudio), bidAdjustments.MediaType.Audio, rules) + buildRulesForMediaType(string(openrtb_ext.BidTypeNative), bidAdjustments.MediaType.Native, rules) + buildRulesForMediaType(VideoInstream, bidAdjustments.MediaType.VideoInstream, rules) + buildRulesForMediaType(VideoOutstream, bidAdjustments.MediaType.VideoOutstream, rules) + buildRulesForMediaType(WildCard, bidAdjustments.MediaType.WildCard, rules) + + return rules +} + +func buildRulesForMediaType(mediaType string, rulesByBidder map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID, rules map[string][]openrtb_ext.Adjustment) { + for bidderName := range rulesByBidder { + for dealID, adjustments := range rulesByBidder[bidderName] { + rule := mediaType + Delimiter + string(bidderName) + Delimiter + dealID + rules[rule] = adjustments + } + } +} + +// Merge takes bid adjustments defined on the request and on the account, and combines/validates them, with the adjustments on the request taking precedence. +func Merge(req *openrtb_ext.RequestWrapper, acctBidAdjs *openrtb_ext.ExtRequestPrebidBidAdjustments) (*openrtb_ext.ExtRequestPrebidBidAdjustments, error) { + mergedBidAdj, err := merge(req, acctBidAdjs) + if err != nil { + return nil, err + } + if !Validate(mergedBidAdj) { + mergedBidAdj = nil + err = &errortypes.Warning{ + WarningCode: errortypes.BidAdjustmentWarningCode, + Message: "bid adjustment on account was invalid", + } + } + return mergedBidAdj, err +} + +func merge(req *openrtb_ext.RequestWrapper, acct *openrtb_ext.ExtRequestPrebidBidAdjustments) (*openrtb_ext.ExtRequestPrebidBidAdjustments, error) { + reqExt, err := req.GetRequestExt() + if err != nil { + return nil, err + } + extPrebid := reqExt.GetPrebid() + + if extPrebid == nil && acct == nil { + return nil, nil + } + if extPrebid == nil && acct != nil { + return acct, nil + } + if extPrebid != nil && acct == nil { + return extPrebid.BidAdjustments, nil + } + + extPrebid.BidAdjustments.MediaType.Banner = mergeForMediaType(extPrebid.BidAdjustments.MediaType.Banner, acct.MediaType.Banner) + extPrebid.BidAdjustments.MediaType.Native = mergeForMediaType(extPrebid.BidAdjustments.MediaType.Native, acct.MediaType.Native) + extPrebid.BidAdjustments.MediaType.Audio = mergeForMediaType(extPrebid.BidAdjustments.MediaType.Audio, acct.MediaType.Audio) + extPrebid.BidAdjustments.MediaType.VideoInstream = mergeForMediaType(extPrebid.BidAdjustments.MediaType.VideoInstream, acct.MediaType.VideoInstream) + extPrebid.BidAdjustments.MediaType.VideoOutstream = mergeForMediaType(extPrebid.BidAdjustments.MediaType.VideoOutstream, acct.MediaType.VideoOutstream) + extPrebid.BidAdjustments.MediaType.WildCard = mergeForMediaType(extPrebid.BidAdjustments.MediaType.WildCard, acct.MediaType.WildCard) + + return extPrebid.BidAdjustments, nil +} + +func mergeForMediaType(reqAdj, acctAdj map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID) map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID { + if reqAdj != nil && acctAdj == nil { + return reqAdj + } + if reqAdj == nil && acctAdj != nil { + return acctAdj + } + + for bidderName, dealIDToAdjustments := range acctAdj { + if _, ok := reqAdj[bidderName]; ok { + for dealID, acctAdjustments := range acctAdj[bidderName] { + if _, ok := reqAdj[bidderName][dealID]; !ok { + reqAdj[bidderName][dealID] = acctAdjustments + } + } + } else { + reqAdj[bidderName] = dealIDToAdjustments + } + } + return reqAdj +} diff --git a/bidadjustment/build_rules_test.go b/bidadjustment/build_rules_test.go new file mode 100644 index 00000000000..263a782130e --- /dev/null +++ b/bidadjustment/build_rules_test.go @@ -0,0 +1,458 @@ +package bidadjustment + +import ( + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestBuildRules(t *testing.T) { + testCases := []struct { + name string + givenBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments + expectedRules map[string][]openrtb_ext.Adjustment + }{ + { + name: "OneAdjustment", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + }, + }, + expectedRules: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + }, + }, + { + name: "MultipleAdjustments", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + "*": { + "diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 1.1, Currency: "USD"}}, + "*": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 5.0, Currency: "USD"}}, + }, + }, + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "*": { + "*": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}, {Type: AdjustmentTypeCPM, Value: 0.18, Currency: "USD"}}, + }, + }, + VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "*": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 0.25, Currency: "USD"}}, + }, + }, + }, + }, + expectedRules: map[string][]openrtb_ext.Adjustment{ + "banner|bidderA|dealId": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + }, + "banner|*|diffDealId": { + { + Type: AdjustmentTypeCPM, + Value: 1.1, + Currency: "USD", + }, + }, + "banner|*|*": { + { + Type: AdjustmentTypeStatic, + Value: 5.0, + Currency: "USD", + }, + }, + "video-instream|*|*": { + { + Type: AdjustmentTypeMultiplier, + Value: 1.1, + }, + { + Type: AdjustmentTypeCPM, + Value: 0.18, + Currency: "USD", + }, + }, + "video-outstream|bidderB|*": { + { + Type: AdjustmentTypeStatic, + Value: 0.25, + Currency: "USD", + }, + }, + }, + }, + { + name: "NilAdjustments", + givenBidAdjustments: nil, + expectedRules: nil, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + rules := BuildRules(test.givenBidAdjustments) + assert.Equal(t, test.expectedRules, rules) + }) + } +} + +func TestMergeAndValidate(t *testing.T) { + testCases := []struct { + name string + givenRequestWrapper *openrtb_ext.RequestWrapper + givenAccount *config.Account + expectError bool + expectedBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments + }{ + { + name: "ValidReqAndAcctAdjustments", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.5}}, + }, + }, + }, + }, + }, + expectError: false, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.1}}, + }, + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.5}}, + }, + }, + }, + }, + }, + { + name: "InvalidReqAdjustment", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 200}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.5}}, + }, + }, + }, + }, + }, + expectError: true, + expectedBidAdjustments: nil, + }, + { + name: "InvalidAcctAdjustment", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: -1.5}}, + }, + }, + }, + }, + }, + expectError: true, + expectedBidAdjustments: nil, + }, + { + name: "InvalidJSON", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{}}`)}, + }, + givenAccount: &config.Account{}, + expectError: true, + expectedBidAdjustments: nil, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + mergedBidAdj, err := Merge(test.givenRequestWrapper, test.givenAccount.BidAdjustments) + if !test.expectError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + assert.Equal(t, test.expectedBidAdjustments, mergedBidAdj) + }) + } +} + +func TestMerge(t *testing.T) { + testCases := []struct { + name string + givenRequestWrapper *openrtb_ext.RequestWrapper + givenAccount *config.Account + expectedBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments + }{ + { + name: "DiffBidderNames", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + { + name: "RequestTakesPrecedence", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"audio":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + }, + }, + }, + { + name: "DiffDealIds", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-instream":{"bidderA":{"dealId":[{ "adjtype": "static", "value": 3.00, "currency": "USD"}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 3.00, Currency: "USD"}}, + "diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + { + name: "DiffBidderNamesCpm", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"native":{"bidderA":{"dealId":[{"adjtype": "cpm", "value": 0.18, "currency": "USD"}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 0.18, Currency: "USD"}}, + }, + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + { + name: "ReqAdjVideoAcctAdjBanner", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-outstream":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + }, + }, + }, + { + name: "RequestNilPrebid", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"ext":{"bidder": {}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + { + name: "AcctWildCardRequestVideo", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-instream":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{ + BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, + }, + }, + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + }, + }, + }, + { + name: "NilReqExtPrebidAndAcctBidAdj", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"ext":{"bidder": {}}}`)}, + }, + givenAccount: &config.Account{}, + expectedBidAdjustments: nil, + }, + { + name: "NilAcctBidAdj", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + givenAccount: &config.Account{}, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + mergedBidAdj, err := merge(test.givenRequestWrapper, test.givenAccount.BidAdjustments) + assert.NoError(t, err) + assert.Equal(t, test.expectedBidAdjustments, mergedBidAdj) + }) + } +} diff --git a/bidadjustment/validate.go b/bidadjustment/validate.go new file mode 100644 index 00000000000..c0ae3d4a27b --- /dev/null +++ b/bidadjustment/validate.go @@ -0,0 +1,70 @@ +package bidadjustment + +import ( + "math" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// Validate checks whether all provided bid adjustments are valid or not against the requirements defined in the issue +func Validate(bidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments) bool { + if bidAdjustments == nil { + return true + } + if !validateForMediaType(bidAdjustments.MediaType.Banner) { + return false + } + if !validateForMediaType(bidAdjustments.MediaType.Audio) { + return false + } + if !validateForMediaType(bidAdjustments.MediaType.VideoInstream) { + return false + } + if !validateForMediaType(bidAdjustments.MediaType.VideoOutstream) { + return false + } + if !validateForMediaType(bidAdjustments.MediaType.Native) { + return false + } + if !validateForMediaType(bidAdjustments.MediaType.WildCard) { + return false + } + return true +} + +func validateForMediaType(bidAdj map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID) bool { + for bidderName := range bidAdj { + if bidAdj[bidderName] == nil { + return false + } + for dealID := range bidAdj[bidderName] { + if bidAdj[bidderName][dealID] == nil { + return false + } + for _, adjustment := range bidAdj[bidderName][dealID] { + if !validateAdjustment(adjustment) { + return false + } + } + } + } + return true +} + +func validateAdjustment(adjustment openrtb_ext.Adjustment) bool { + switch adjustment.Type { + case AdjustmentTypeCPM: + if adjustment.Currency != "" && adjustment.Value >= 0 && adjustment.Value < math.MaxFloat64 { + return true + } + case AdjustmentTypeMultiplier: + if adjustment.Value >= 0 && adjustment.Value < 100 { + return true + } + case AdjustmentTypeStatic: + if adjustment.Currency != "" && adjustment.Value >= 0 && adjustment.Value < math.MaxFloat64 { + return true + } + } + return false +} diff --git a/bidadjustment/validate_test.go b/bidadjustment/validate_test.go new file mode 100644 index 00000000000..a0b4eb436eb --- /dev/null +++ b/bidadjustment/validate_test.go @@ -0,0 +1,338 @@ +package bidadjustment + +import ( + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + testCases := []struct { + name string + givenBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments + expected bool + }{ + { + name: "OneAdjustmentValid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + }, + }, + expected: true, + }, + { + name: "MultipleAdjustmentsValid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 3.0, Currency: "USD"}}, + }, + }, + }, + }, + expected: true, + }, + { + name: "MixOfValidandInvalid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: ""}}, + }, + }, + }, + }, + expected: false, + }, + { + name: "WildCardInvalid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: -1.1, Currency: "USD"}}, + }, + }, + }, + }, + expected: false, + }, + { + name: "AudioInvalid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 5.0, Currency: ""}}, + }, + }, + }, + }, + expected: false, + }, + { + name: "NativeInvalid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: -1.1, Currency: "USD"}}, + }, + }, + }, + }, + expected: false, + }, + { + name: "BannerInvalid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 150}}, + }, + }, + }, + }, + expected: false, + }, + { + name: "InstreamInvalid", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 150}}, + }, + }, + }, + }, + expected: false, + }, + { + name: "EmptyBidAdjustments", + givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{}, + expected: true, + }, + { + name: "NilBidAdjustments", + givenBidAdjustments: nil, + expected: true, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actual := Validate(test.givenBidAdjustments) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestValidateForMediaType(t *testing.T) { + testCases := []struct { + name string + givenBidAdj map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID + expected bool + }{ + { + name: "OneAdjustmentValid", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, + }, + }, + expected: true, + }, + { + name: "OneAdjustmentInvalid", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: -1.1}}, + }, + }, + expected: false, + }, + { + name: "MultipleAdjustmentsValid", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{ + {Type: AdjustmentTypeMultiplier, Value: 1.1}, + {Type: AdjustmentTypeStatic, Value: 3.0, Currency: "USD"}, + }, + }, + }, + expected: true, + }, + { + name: "MultipleAdjustmentsInvalid", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{ + {Type: AdjustmentTypeMultiplier, Value: -1.1}, + {Type: AdjustmentTypeCPM, Value: -3.0, Currency: "USD"}, + }, + }, + }, + expected: false, + }, + { + name: "MultipleDealIdsValid", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{ + {Type: AdjustmentTypeStatic, Value: 3.0, Currency: "USD"}, + }, + "diffDealId": []openrtb_ext.Adjustment{ + {Type: AdjustmentTypeMultiplier, Value: 1.1}, + }, + }, + }, + expected: true, + }, + { + name: "MultipleBiddersValid", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": []openrtb_ext.Adjustment{ + {Type: AdjustmentTypeStatic, Value: 5.0, Currency: "USD"}, + }, + }, + "bidderB": { + "dealId": []openrtb_ext.Adjustment{ + {Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}, + }, + }, + }, + expected: true, + }, + { + name: "NilBidAdj", + givenBidAdj: nil, + expected: true, + }, + { + name: "NilBidderToAdjustmentsByDealID", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": nil, + }, + expected: false, + }, + { + name: "NilDealIdToAdjustments", + givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": { + "dealId": nil, + }, + }, + expected: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actual := validateForMediaType(test.givenBidAdj) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestValidateAdjustment(t *testing.T) { + testCases := []struct { + name string + givenAdjustment openrtb_ext.Adjustment + expected bool + }{ + { + name: "ValidCpm", + givenAdjustment: openrtb_ext.Adjustment{ + Type: AdjustmentTypeCPM, + Value: 5.0, + Currency: "USD", + }, + expected: true, + }, + { + name: "ValidMultiplier", + givenAdjustment: openrtb_ext.Adjustment{ + Type: AdjustmentTypeMultiplier, + Value: 2.0, + }, + expected: true, + }, + { + name: "ValidStatic", + givenAdjustment: openrtb_ext.Adjustment{ + Type: AdjustmentTypeStatic, + Value: 3.0, + Currency: "USD", + }, + expected: true, + }, + { + name: "InvalidCpm", + givenAdjustment: openrtb_ext.Adjustment{ + Type: AdjustmentTypeCPM, + Value: 5.0, + Currency: "", + }, + expected: false, + }, + { + name: "InvalidMultiplier", + givenAdjustment: openrtb_ext.Adjustment{ + Type: AdjustmentTypeMultiplier, + Value: 200, + }, + expected: false, + }, + { + name: "InvalidStatic", + givenAdjustment: openrtb_ext.Adjustment{ + Type: AdjustmentTypeStatic, + Value: -3.0, + Currency: "USD", + }, + expected: false, + }, + { + name: "InvalidAdjType", + givenAdjustment: openrtb_ext.Adjustment{ + Type: "Invalid", + Value: 1.0, + }, + expected: false, + }, + { + name: "EmptyAdjustment", + givenAdjustment: openrtb_ext.Adjustment{}, + expected: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actual := validateAdjustment(test.givenAdjustment) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/config/accounts.go b/config/account.go similarity index 78% rename from config/accounts.go rename to config/account.go index 77803309aad..038a8daaf9e 100644 --- a/config/accounts.go +++ b/config/account.go @@ -8,6 +8,7 @@ import ( "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/util/iputil" ) // ChannelType enumerates the values of integrations Prebid Server can configure for an account @@ -23,22 +24,24 @@ const ( // Account represents a publisher account configuration type Account struct { - ID string `mapstructure:"id" json:"id"` - Disabled bool `mapstructure:"disabled" json:"disabled"` - CacheTTL DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"` - EventsEnabled bool `mapstructure:"events_enabled" json:"events_enabled"` - CCPA AccountCCPA `mapstructure:"ccpa" json:"ccpa"` - GDPR AccountGDPR `mapstructure:"gdpr" json:"gdpr"` - DebugAllow bool `mapstructure:"debug_allow" json:"debug_allow"` - DefaultIntegration string `mapstructure:"default_integration" json:"default_integration"` - CookieSync CookieSync `mapstructure:"cookie_sync" json:"cookie_sync"` - Events Events `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725 - TruncateTargetAttribute *int `mapstructure:"truncate_target_attr" json:"truncate_target_attr"` - AlternateBidderCodes *openrtb_ext.ExtAlternateBidderCodes `mapstructure:"alternatebiddercodes" json:"alternatebiddercodes"` - Hooks AccountHooks `mapstructure:"hooks" json:"hooks"` - PriceFloors AccountPriceFloors `mapstructure:"price_floors" json:"price_floors"` - Validations Validations `mapstructure:"validations" json:"validations"` - DefaultBidLimit int `mapstructure:"default_bid_limit" json:"default_bid_limit"` + ID string `mapstructure:"id" json:"id"` + Disabled bool `mapstructure:"disabled" json:"disabled"` + CacheTTL DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"` + EventsEnabled *bool `mapstructure:"events_enabled" json:"events_enabled"` // Deprecated: Use events.enabled instead. + CCPA AccountCCPA `mapstructure:"ccpa" json:"ccpa"` + GDPR AccountGDPR `mapstructure:"gdpr" json:"gdpr"` + DebugAllow bool `mapstructure:"debug_allow" json:"debug_allow"` + DefaultIntegration string `mapstructure:"default_integration" json:"default_integration"` + CookieSync CookieSync `mapstructure:"cookie_sync" json:"cookie_sync"` + Events Events `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725 + TruncateTargetAttribute *int `mapstructure:"truncate_target_attr" json:"truncate_target_attr"` + AlternateBidderCodes *openrtb_ext.ExtAlternateBidderCodes `mapstructure:"alternatebiddercodes" json:"alternatebiddercodes"` + Hooks AccountHooks `mapstructure:"hooks" json:"hooks"` + PriceFloors AccountPriceFloors `mapstructure:"price_floors" json:"price_floors"` + Validations Validations `mapstructure:"validations" json:"validations"` + DefaultBidLimit int `mapstructure:"default_bid_limit" json:"default_bid_limit"` + BidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments `mapstructure:"bidadjustments" json:"bidadjustments"` + Privacy AccountPrivacy `mapstructure:"privacy" json:"privacy"` } // CookieSync represents the account-level defaults for the cookie sync endpoint. @@ -163,7 +166,8 @@ func (a *AccountGDPR) PurposeEnforced(purpose consentconstants.Purpose) (value, // PurposeEnforcementAlgo checks the purpose enforcement algo for a given purpose by first // looking at the account settings, and if not set there, defaulting to the host configuration. func (a *AccountGDPR) PurposeEnforcementAlgo(purpose consentconstants.Purpose) (value TCF2EnforcementAlgo, exists bool) { - c, exists := a.PurposeConfigs[purpose] + var c *AccountGDPRPurpose + c, exists = a.PurposeConfigs[purpose] if exists && (c.EnforceAlgoID == TCF2BasicEnforcement || c.EnforceAlgoID == TCF2FullEnforcement) { return c.EnforceAlgoID, true @@ -290,3 +294,33 @@ func (m AccountModules) ModuleConfig(id string) (json.RawMessage, error) { func (a *AccountChannel) IsSet() bool { return a.AMP != nil || a.App != nil || a.Video != nil || a.Web != nil } + +type AccountPrivacy struct { + AllowActivities *AllowActivities `mapstructure:"allowactivities" json:"allowactivities"` + IPv6Config IPv6 `mapstructure:"ipv6" json:"ipv6"` + IPv4Config IPv4 `mapstructure:"ipv4" json:"ipv4"` +} + +type IPv6 struct { + AnonKeepBits int `mapstructure:"anon_keep_bits" json:"anon_keep_bits"` +} + +type IPv4 struct { + AnonKeepBits int `mapstructure:"anon_keep_bits" json:"anon_keep_bits"` +} + +func (ip *IPv6) Validate(errs []error) []error { + if ip.AnonKeepBits > iputil.IPv6BitSize || ip.AnonKeepBits < 0 { + err := fmt.Errorf("bits cannot exceed %d in ipv6 address, or be less than 0", iputil.IPv6BitSize) + errs = append(errs, err) + } + return errs +} + +func (ip *IPv4) Validate(errs []error) []error { + if ip.AnonKeepBits > iputil.IPv4BitSize || ip.AnonKeepBits < 0 { + err := fmt.Errorf("bits cannot exceed %d in ipv4 address, or be less than 0", iputil.IPv4BitSize) + errs = append(errs, err) + } + return errs +} diff --git a/config/accounts_test.go b/config/account_test.go similarity index 96% rename from config/accounts_test.go rename to config/account_test.go index 20c6053246e..31d9796a622 100644 --- a/config/accounts_test.go +++ b/config/account_test.go @@ -910,3 +910,49 @@ func TestAccountPriceFloorsValidate(t *testing.T) { }) } } + +func TestIPMaskingValidate(t *testing.T) { + tests := []struct { + name string + privacy AccountPrivacy + want []error + }{ + { + name: "valid", + privacy: AccountPrivacy{ + IPv4Config: IPv4{AnonKeepBits: 1}, + IPv6Config: IPv6{AnonKeepBits: 0}, + }, + }, + { + name: "invalid", + privacy: AccountPrivacy{ + IPv4Config: IPv4{AnonKeepBits: -100}, + IPv6Config: IPv6{AnonKeepBits: -200}, + }, + want: []error{ + errors.New("bits cannot exceed 32 in ipv4 address, or be less than 0"), + errors.New("bits cannot exceed 128 in ipv6 address, or be less than 0"), + }, + }, + { + name: "mixed", + privacy: AccountPrivacy{ + IPv4Config: IPv4{AnonKeepBits: 10}, + IPv6Config: IPv6{AnonKeepBits: -10}, + }, + want: []error{ + errors.New("bits cannot exceed 128 in ipv6 address, or be less than 0"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var errs []error + errs = tt.privacy.IPv4Config.Validate(errs) + errs = tt.privacy.IPv6Config.Validate(errs) + assert.ElementsMatch(t, errs, tt.want) + }) + } +} diff --git a/config/activity.go b/config/activity.go new file mode 100644 index 00000000000..5bddc7c6405 --- /dev/null +++ b/config/activity.go @@ -0,0 +1,27 @@ +package config + +type AllowActivities struct { + SyncUser Activity `mapstructure:"syncUser" json:"syncUser"` + FetchBids Activity `mapstructure:"fetchBids" json:"fetchBids"` + EnrichUserFPD Activity `mapstructure:"enrichUfpd" json:"enrichUfpd"` + ReportAnalytics Activity `mapstructure:"reportAnalytics" json:"reportAnalytics"` + TransmitUserFPD Activity `mapstructure:"transmitUfpd" json:"transmitUfpd"` + TransmitPreciseGeo Activity `mapstructure:"transmitPreciseGeo" json:"transmitPreciseGeo"` + TransmitUniqueRequestIds Activity `mapstructure:"transmitUniqueRequestIds" json:"transmitUniqueRequestIds"` + TransmitTids Activity `mapstructure:"transmitTid" json:"transmitTid"` +} + +type Activity struct { + Default *bool `mapstructure:"default" json:"default"` + Rules []ActivityRule `mapstructure:"rules" json:"rules"` +} + +type ActivityRule struct { + Condition ActivityCondition `mapstructure:"condition" json:"condition"` + Allow bool `mapstructure:"allow" json:"allow"` +} + +type ActivityCondition struct { + ComponentName []string `mapstructure:"componentName" json:"componentName"` + ComponentType []string `mapstructure:"componentType" json:"componentType"` +} diff --git a/config/bidderinfo.go b/config/bidderinfo.go index 6f62488c878..96d6fd15bfc 100644 --- a/config/bidderinfo.go +++ b/config/bidderinfo.go @@ -23,6 +23,7 @@ type BidderInfos map[string]BidderInfo // BidderInfo specifies all configuration for a bidder except for enabled status, endpoint, and extra information. type BidderInfo struct { + AliasOf string `yaml:"aliasOf" mapstructure:"aliasOf"` Disabled bool `yaml:"disabled" mapstructure:"disabled"` Endpoint string `yaml:"endpoint" mapstructure:"endpoint"` ExtraAdapterInfo string `yaml:"extra_info" mapstructure:"extra_info"` @@ -51,6 +52,13 @@ type BidderInfo struct { OpenRTB *OpenRTBInfo `yaml:"openrtb" mapstructure:"openrtb"` } +type aliasNillableFields struct { + Disabled *bool `yaml:"disabled" mapstructure:"disabled"` + ModifyingVastXmlAllowed *bool `yaml:"modifyingVastXmlAllowed" mapstructure:"modifyingVastXmlAllowed"` + Experiment *BidderInfoExperiment `yaml:"experiment" mapstructure:"experiment"` + XAPI *AdapterXAPI `yaml:"xapi" mapstructure:"xapi"` +} + // BidderInfoExperiment specifies non-production ready feature config for a bidder type BidderInfoExperiment struct { AdsCert BidderAdsCert `yaml:"adsCert" mapstructure:"adsCert"` @@ -161,7 +169,7 @@ type SyncerEndpoint struct { // startup: // // {{.ExternalURL}} - This will be replaced with the host server's externally reachable http path. - // {{.SyncerKey}} - This will be replaced with the syncer key. + // {{.BidderName}} - This will be replaced with the bidder name. // {{.SyncType}} - This will be replaced with the sync type, either 'b' for iframe syncs or 'i' // for redirect/image syncs. // {{.UserMacro}} - This will be replaced with the bidder server's user id macro. @@ -228,24 +236,111 @@ func processBidderInfos(reader InfoReader, normalizeBidderName func(string) (ope return nil, fmt.Errorf("error loading bidders data") } - infos := BidderInfos{} - + bidderInfos := BidderInfos{} + aliasNillableFieldsByBidder := map[string]aliasNillableFields{} for fileName, data := range bidderConfigs { bidderName := strings.Split(fileName, ".") if len(bidderName) == 2 && bidderName[1] == "yaml" { - normalizedBidderName, bidderNameExists := normalizeBidderName(bidderName[0]) - if !bidderNameExists { - return nil, fmt.Errorf("error parsing config for bidder %s: unknown bidder", fileName) - } info := BidderInfo{} if err := yaml.Unmarshal(data, &info); err != nil { return nil, fmt.Errorf("error parsing config for bidder %s: %v", fileName, err) } - infos[string(normalizedBidderName)] = info + //need to maintain nullable fields from BidderInfo struct into bidderInfoNullableFields + //to handle the default values in aliases yaml + if len(info.AliasOf) > 0 { + aliasFields := aliasNillableFields{} + if err := yaml.Unmarshal(data, &aliasFields); err != nil { + return nil, fmt.Errorf("error parsing config for aliased bidder %s: %v", fileName, err) + } + + //required for CoreBidderNames function to also return aliasBiddernames + if err := openrtb_ext.SetAliasBidderName(bidderName[0], openrtb_ext.BidderName(info.AliasOf)); err != nil { + return nil, err + } + + normalizedBidderName, bidderNameExists := normalizeBidderName(bidderName[0]) + if !bidderNameExists { + return nil, fmt.Errorf("error parsing config for an alias %s: unknown bidder", fileName) + } + + aliasNillableFieldsByBidder[string(normalizedBidderName)] = aliasFields + bidderInfos[string(normalizedBidderName)] = info + } else { + normalizedBidderName, bidderNameExists := normalizeBidderName(bidderName[0]) + if !bidderNameExists { + return nil, fmt.Errorf("error parsing config for bidder %s: unknown bidder", fileName) + } + + bidderInfos[string(normalizedBidderName)] = info + } } } - return infos, nil + return processBidderAliases(aliasNillableFieldsByBidder, bidderInfos) +} + +func processBidderAliases(aliasNillableFieldsByBidder map[string]aliasNillableFields, bidderInfos BidderInfos) (BidderInfos, error) { + for bidderName, alias := range aliasNillableFieldsByBidder { + aliasBidderInfo, ok := bidderInfos[bidderName] + if !ok { + return nil, fmt.Errorf("bidder info not found for an alias: %s", bidderName) + } + if err := validateAliases(aliasBidderInfo, bidderInfos, bidderName); err != nil { + return nil, err + } + + parentBidderInfo := bidderInfos[aliasBidderInfo.AliasOf] + if aliasBidderInfo.AppSecret == "" { + aliasBidderInfo.AppSecret = parentBidderInfo.AppSecret + } + if aliasBidderInfo.Capabilities == nil { + aliasBidderInfo.Capabilities = parentBidderInfo.Capabilities + } + if aliasBidderInfo.Debug == nil { + aliasBidderInfo.Debug = parentBidderInfo.Debug + } + if aliasBidderInfo.Endpoint == "" { + aliasBidderInfo.Endpoint = parentBidderInfo.Endpoint + } + if aliasBidderInfo.EndpointCompression == "" { + aliasBidderInfo.EndpointCompression = parentBidderInfo.EndpointCompression + } + if aliasBidderInfo.ExtraAdapterInfo == "" { + aliasBidderInfo.ExtraAdapterInfo = parentBidderInfo.ExtraAdapterInfo + } + if aliasBidderInfo.GVLVendorID == 0 { + aliasBidderInfo.GVLVendorID = parentBidderInfo.GVLVendorID + } + if aliasBidderInfo.Maintainer == nil { + aliasBidderInfo.Maintainer = parentBidderInfo.Maintainer + } + if aliasBidderInfo.OpenRTB == nil { + aliasBidderInfo.OpenRTB = parentBidderInfo.OpenRTB + } + if aliasBidderInfo.PlatformID == "" { + aliasBidderInfo.PlatformID = parentBidderInfo.PlatformID + } + if aliasBidderInfo.Syncer == nil { + aliasBidderInfo.Syncer = parentBidderInfo.Syncer + } + if aliasBidderInfo.UserSyncURL == "" { + aliasBidderInfo.UserSyncURL = parentBidderInfo.UserSyncURL + } + if alias.Disabled == nil { + aliasBidderInfo.Disabled = parentBidderInfo.Disabled + } + if alias.Experiment == nil { + aliasBidderInfo.Experiment = parentBidderInfo.Experiment + } + if alias.ModifyingVastXmlAllowed == nil { + aliasBidderInfo.ModifyingVastXmlAllowed = parentBidderInfo.ModifyingVastXmlAllowed + } + if alias.XAPI == nil { + aliasBidderInfo.XAPI = parentBidderInfo.XAPI + } + bidderInfos[bidderName] = aliasBidderInfo + } + return bidderInfos, nil } // ToGVLVendorIDMap transforms a BidderInfos object to a map of bidder names to GVL id. @@ -266,20 +361,31 @@ func (infos BidderInfos) validate(errs []error) []error { if bidder.IsEnabled() { errs = validateAdapterEndpoint(bidder.Endpoint, bidderName, errs) - validateInfoErr := validateInfo(bidder, bidderName) - if validateInfoErr != nil { - errs = append(errs, validateInfoErr) + if err := validateInfo(bidder, infos, bidderName); err != nil { + errs = append(errs, err) } - validateSyncerErr := validateSyncer(bidder) - if validateSyncerErr != nil { - errs = append(errs, validateSyncerErr) + if err := validateSyncer(bidder); err != nil { + errs = append(errs, err) } } } return errs } +func validateAliases(aliasBidderInfo BidderInfo, infos BidderInfos, bidderName string) error { + if len(aliasBidderInfo.AliasOf) > 0 { + if parentBidder, ok := infos[aliasBidderInfo.AliasOf]; ok { + if len(parentBidder.AliasOf) > 0 { + return fmt.Errorf("bidder: %s cannot be an alias of an alias: %s", aliasBidderInfo.AliasOf, bidderName) + } + } else { + return fmt.Errorf("bidder: %s not found for an alias: %s", aliasBidderInfo.AliasOf, bidderName) + } + } + return nil +} + var testEndpointTemplateParams = macros.EndpointTemplateParams{ Host: "anyHost", PublisherID: "anyPublisherID", @@ -322,14 +428,18 @@ func validateAdapterEndpoint(endpoint string, bidderName string, errs []error) [ return errs } -func validateInfo(info BidderInfo, bidderName string) error { - if err := validateMaintainer(info.Maintainer, bidderName); err != nil { +func validateInfo(bidder BidderInfo, infos BidderInfos, bidderName string) error { + if err := validateMaintainer(bidder.Maintainer, bidderName); err != nil { return err } - if err := validateCapabilities(info.Capabilities, bidderName); err != nil { + if err := validateCapabilities(bidder.Capabilities, bidderName); err != nil { return err } - + if len(bidder.AliasOf) > 0 { + if err := validateAliasCapabilities(bidder, infos, bidderName); err != nil { + return err + } + } return nil } @@ -340,6 +450,52 @@ func validateMaintainer(info *MaintainerInfo, bidderName string) error { return nil } +func validateAliasCapabilities(aliasBidderInfo BidderInfo, infos BidderInfos, bidderName string) error { + parentBidder, parentFound := infos[aliasBidderInfo.AliasOf] + if !parentFound { + return fmt.Errorf("parent bidder: %s not found for an alias: %s", aliasBidderInfo.AliasOf, bidderName) + } + + if aliasBidderInfo.Capabilities != nil { + if parentBidder.Capabilities == nil { + return fmt.Errorf("capabilities for alias: %s should be a subset of capabilities for parent bidder: %s", bidderName, aliasBidderInfo.AliasOf) + } + + if (aliasBidderInfo.Capabilities.App != nil && parentBidder.Capabilities.App == nil) || (aliasBidderInfo.Capabilities.Site != nil && parentBidder.Capabilities.Site == nil) { + return fmt.Errorf("capabilities for alias: %s should be a subset of capabilities for parent bidder: %s", bidderName, aliasBidderInfo.AliasOf) + } + + if aliasBidderInfo.Capabilities.Site != nil && parentBidder.Capabilities.Site != nil { + if err := isAliasPlatformInfoSubsetOfParent(*parentBidder.Capabilities.Site, *aliasBidderInfo.Capabilities.Site, bidderName, aliasBidderInfo.AliasOf); err != nil { + return err + } + } + + if aliasBidderInfo.Capabilities.App != nil && parentBidder.Capabilities.App != nil { + if err := isAliasPlatformInfoSubsetOfParent(*parentBidder.Capabilities.App, *aliasBidderInfo.Capabilities.App, bidderName, aliasBidderInfo.AliasOf); err != nil { + return err + } + } + } + + return nil +} + +func isAliasPlatformInfoSubsetOfParent(parentInfo PlatformInfo, aliasInfo PlatformInfo, bidderName string, parentBidderName string) error { + parentMediaTypes := make(map[openrtb_ext.BidType]struct{}) + for _, info := range parentInfo.MediaTypes { + parentMediaTypes[info] = struct{}{} + } + + for _, info := range aliasInfo.MediaTypes { + if _, found := parentMediaTypes[info]; !found { + return fmt.Errorf("mediaTypes for alias: %s should be a subset of MediaTypes for parent bidder: %s", bidderName, parentBidderName) + } + } + + return nil +} + func validateCapabilities(info *CapabilitiesInfo, bidderName string) error { if info == nil { return fmt.Errorf("missing required field: capabilities for adapter: %s", bidderName) diff --git a/config/bidderinfo_test.go b/config/bidderinfo_test.go index 6005178c605..77625c0cdd0 100644 --- a/config/bidderinfo_test.go +++ b/config/bidderinfo_test.go @@ -42,6 +42,27 @@ endpointCompression: GZIP openrtb: version: 2.6 gpp-supported: true +endpoint: https://endpoint.com +disabled: false +extra_info: extra-info +app_secret: app-secret +platform_id: 123 +usersync_url: user-url +userSync: + key: foo + default: iframe + iframe: + url: https://foo.com/sync?mode=iframe&r={{.RedirectURL}} + redirectUrl: https://redirect/setuid/iframe + externalUrl: https://iframe.host + userMacro: UID +xapi: + username: uname + password: pwd + tracker: tracker +` +const testSimpleAliasYAML = ` +aliasOf: bidderA ` func TestLoadBidderInfoFromDisk(t *testing.T) { @@ -131,6 +152,120 @@ func TestProcessBidderInfo(t *testing.T) { expectedBidderInfos: nil, expectError: "error parsing config for bidder bidderA.yaml", }, + { + description: "Invalid alias name", + bidderInfos: map[string][]byte{ + "all.yaml": []byte(testSimpleAliasYAML), + }, + expectedBidderInfos: nil, + expectError: "alias all is a reserved bidder name and cannot be used", + }, + { + description: "Valid aliases", + bidderInfos: map[string][]byte{ + "bidderA.yaml": []byte(fullBidderYAMLConfig), + "bidderB.yaml": []byte(testSimpleAliasYAML), + }, + expectedBidderInfos: BidderInfos{ + "bidderA": BidderInfo{ + AppSecret: "app-secret", + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, + }, + }, + Debug: &DebugInfo{ + Allow: true, + }, + Disabled: false, + Endpoint: "https://endpoint.com", + EndpointCompression: "GZIP", + Experiment: BidderInfoExperiment{ + AdsCert: BidderAdsCert{ + Enabled: true, + }, + }, + ExtraAdapterInfo: "extra-info", + GVLVendorID: 42, + Maintainer: &MaintainerInfo{ + Email: "some-email@domain.com", + }, + ModifyingVastXmlAllowed: true, + OpenRTB: &OpenRTBInfo{ + GPPSupported: true, + Version: "2.6", + }, + PlatformID: "123", + Syncer: &Syncer{ + Key: "foo", + IFrame: &SyncerEndpoint{ + URL: "https://foo.com/sync?mode=iframe&r={{.RedirectURL}}", + RedirectURL: "https://redirect/setuid/iframe", + ExternalURL: "https://iframe.host", + UserMacro: "UID", + }, + }, + UserSyncURL: "user-url", + XAPI: AdapterXAPI{ + Username: "uname", + Password: "pwd", + Tracker: "tracker", + }, + }, + "bidderB": BidderInfo{ + AliasOf: "bidderA", + AppSecret: "app-secret", + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, + }, + }, + Debug: &DebugInfo{ + Allow: true, + }, + Disabled: false, + Endpoint: "https://endpoint.com", + EndpointCompression: "GZIP", + Experiment: BidderInfoExperiment{ + AdsCert: BidderAdsCert{ + Enabled: true, + }, + }, + ExtraAdapterInfo: "extra-info", + GVLVendorID: 42, + Maintainer: &MaintainerInfo{ + Email: "some-email@domain.com", + }, + ModifyingVastXmlAllowed: true, + OpenRTB: &OpenRTBInfo{ + GPPSupported: true, + Version: "2.6", + }, + PlatformID: "123", + Syncer: &Syncer{ + Key: "foo", + IFrame: &SyncerEndpoint{ + URL: "https://foo.com/sync?mode=iframe&r={{.RedirectURL}}", + RedirectURL: "https://redirect/setuid/iframe", + ExternalURL: "https://iframe.host", + UserMacro: "UID", + }, + }, + UserSyncURL: "user-url", + XAPI: AdapterXAPI{ + Username: "uname", + Password: "pwd", + Tracker: "tracker", + }, + }, + }, + }, } for _, test := range testCases { reader := StubInfoReader{test.bidderInfos} @@ -145,6 +280,180 @@ func TestProcessBidderInfo(t *testing.T) { } +func TestProcessAliasBidderInfo(t *testing.T) { + parentBidderInfo := BidderInfo{ + AppSecret: "app-secret", + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, + }, + }, + Debug: &DebugInfo{ + Allow: true, + }, + Disabled: false, + Endpoint: "https://endpoint.com", + EndpointCompression: "GZIP", + Experiment: BidderInfoExperiment{ + AdsCert: BidderAdsCert{ + Enabled: true, + }, + }, + ExtraAdapterInfo: "extra-info", + GVLVendorID: 42, + Maintainer: &MaintainerInfo{ + Email: "some-email@domain.com", + }, + ModifyingVastXmlAllowed: true, + OpenRTB: &OpenRTBInfo{ + GPPSupported: true, + Version: "2.6", + }, + PlatformID: "123", + Syncer: &Syncer{ + Key: "foo", + IFrame: &SyncerEndpoint{ + URL: "https://foo.com/sync?mode=iframe&r={{.RedirectURL}}", + RedirectURL: "https://redirect/setuid/iframe", + ExternalURL: "https://iframe.host", + UserMacro: "UID", + }, + }, + UserSyncURL: "user-url", + XAPI: AdapterXAPI{ + Username: "uname", + Password: "pwd", + Tracker: "tracker", + }, + } + aliasBidderInfo := BidderInfo{ + AppSecret: "alias-app-secret", + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}, + }, + }, + Debug: &DebugInfo{ + Allow: false, + }, + Disabled: true, + Endpoint: "https://alias-endpoint.com", + EndpointCompression: "DEFAULT", + Experiment: BidderInfoExperiment{ + AdsCert: BidderAdsCert{ + Enabled: false, + }, + }, + ExtraAdapterInfo: "alias-extra-info", + GVLVendorID: 43, + Maintainer: &MaintainerInfo{ + Email: "alias-email@domain.com", + }, + ModifyingVastXmlAllowed: false, + OpenRTB: &OpenRTBInfo{ + GPPSupported: false, + Version: "2.5", + }, + PlatformID: "456", + Syncer: &Syncer{ + Key: "alias", + IFrame: &SyncerEndpoint{ + URL: "https://alias.com/sync?mode=iframe&r={{.RedirectURL}}", + RedirectURL: "https://alias-redirect/setuid/iframe", + ExternalURL: "https://alias-iframe.host", + UserMacro: "alias-UID", + }, + }, + UserSyncURL: "alias-user-url", + XAPI: AdapterXAPI{ + Username: "alias-uname", + Password: "alias-pwd", + Tracker: "alias-tracker", + }, + } + bidderB := parentBidderInfo + bidderB.AliasOf = "bidderA" + testCases := []struct { + description string + aliasInfos map[string]aliasNillableFields + bidderInfos BidderInfos + expectedBidderInfos BidderInfos + expectedErr error + }{ + { + description: "inherit all parent info in alias bidder", + aliasInfos: map[string]aliasNillableFields{ + "bidderB": { + Disabled: nil, + ModifyingVastXmlAllowed: nil, + Experiment: nil, + XAPI: nil, + }, + }, + bidderInfos: BidderInfos{ + "bidderA": parentBidderInfo, + "bidderB": BidderInfo{ + AliasOf: "bidderA", + // all other fields should be inherited from parent bidder + }, + }, + expectedErr: nil, + expectedBidderInfos: BidderInfos{"bidderA": parentBidderInfo, "bidderB": bidderB}, + }, + { + description: "all bidder info specified for alias, do not inherit from parent bidder", + aliasInfos: map[string]aliasNillableFields{ + "bidderB": { + Disabled: &aliasBidderInfo.Disabled, + ModifyingVastXmlAllowed: &aliasBidderInfo.ModifyingVastXmlAllowed, + Experiment: &aliasBidderInfo.Experiment, + XAPI: &aliasBidderInfo.XAPI, + }, + }, + bidderInfos: BidderInfos{ + "bidderA": parentBidderInfo, + "bidderB": aliasBidderInfo, + }, + expectedErr: nil, + expectedBidderInfos: BidderInfos{"bidderA": parentBidderInfo, "bidderB": aliasBidderInfo}, + }, + { + description: "invalid alias", + aliasInfos: map[string]aliasNillableFields{ + "bidderB": {}, + }, + bidderInfos: BidderInfos{ + "bidderB": BidderInfo{ + AliasOf: "bidderA", + }, + }, + expectedErr: errors.New("bidder: bidderA not found for an alias: bidderB"), + }, + { + description: "bidder info not found for an alias", + aliasInfos: map[string]aliasNillableFields{ + "bidderB": {}, + }, + expectedErr: errors.New("bidder info not found for an alias: bidderB"), + }, + } + + for _, test := range testCases { + bidderInfos, err := processBidderAliases(test.aliasInfos, test.bidderInfos) + if test.expectedErr != nil { + assert.Equal(t, test.expectedErr, err) + } else { + assert.Equal(t, test.expectedBidderInfos, bidderInfos) + } + } +} + type StubInfoReader struct { mockBidderInfos map[string][]byte } @@ -244,11 +553,78 @@ func TestBidderInfoValidationPositive(t *testing.T) { }, }, }, + "bidderC": BidderInfo{ + Endpoint: "http://bidderB.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + openrtb_ext.BidTypeNative, + openrtb_ext.BidTypeBanner, + }, + }, + }, + AliasOf: "bidderB", + }, } errs := bidderInfos.validate(make([]error, 0)) assert.Len(t, errs, 0, "All bidder infos should be correct") } +func TestValidateAliases(t *testing.T) { + testCase := struct { + description string + bidderInfos BidderInfos + expectErrors []error + }{ + description: "invalid aliases", + bidderInfos: BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + AliasOf: "bidderB", + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + AliasOf: "bidderC", + }, + }, + expectErrors: []error{ + errors.New("bidder: bidderB cannot be an alias of an alias: bidderA"), + errors.New("bidder: bidderC not found for an alias: bidderB"), + }, + } + + var errs []error + for bidderName, bidderInfo := range testCase.bidderInfos { + errs = append(errs, validateAliases(bidderInfo, testCase.bidderInfos, bidderName)) + } + + assert.ElementsMatch(t, errs, testCase.expectErrors) +} + func TestBidderInfoValidationNegative(t *testing.T) { testCases := []struct { description string @@ -536,6 +912,332 @@ func TestBidderInfoValidationNegative(t *testing.T) { errors.New("The endpoint: incorrect for bidderB is not a valid URL"), }, }, + { + "Invalid alias Site capabilities", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("capabilities for alias: bidderB should be a subset of capabilities for parent bidder: bidderA"), + }, + }, + { + "Invalid alias App capabilities", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("capabilities for alias: bidderB should be a subset of capabilities for parent bidder: bidderA"), + }, + }, + { + "Invalid alias capabilities", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{}, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("at least one of capabilities.site or capabilities.app must exist for adapter: bidderA"), + errors.New("capabilities for alias: bidderB should be a subset of capabilities for parent bidder: bidderA"), + }, + }, + { + "Invalid alias MediaTypes for site", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("mediaTypes for alias: bidderB should be a subset of MediaTypes for parent bidder: bidderA"), + }, + }, + { + "Invalid alias MediaTypes for app", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeVideo, + }, + }, + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("mediaTypes for alias: bidderB should be a subset of MediaTypes for parent bidder: bidderA"), + }, + }, + { + "Invalid parent bidder capabilities", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("missing required field: capabilities for adapter: bidderA"), + errors.New("capabilities for alias: bidderB should be a subset of capabilities for parent bidder: bidderA"), + }, + }, + { + "Invalid site alias capabilities with both site and app", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeNative, + }, + }, + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("mediaTypes for alias: bidderB should be a subset of MediaTypes for parent bidder: bidderA"), + }, + }, + { + "Invalid app alias capabilities with both site and app", + BidderInfos{ + "bidderA": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeNative, + }, + }, + }, + }, + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + }, + AliasOf: "bidderA", + }, + }, + []error{ + errors.New("mediaTypes for alias: bidderB should be a subset of MediaTypes for parent bidder: bidderA"), + }, + }, + { + "Invalid parent bidder for alias", + BidderInfos{ + "bidderB": BidderInfo{ + Endpoint: "http://bidderA.com/openrtb2", + Maintainer: &MaintainerInfo{ + Email: "maintainer@bidderA.com", + }, + Capabilities: &CapabilitiesInfo{ + App: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + Site: &PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeNative, + }, + }, + }, + AliasOf: "bidderC", + }, + }, + []error{ + errors.New("parent bidder: bidderC not found for an alias: bidderB"), + }, + }, } for _, test := range testCases { @@ -974,7 +1676,6 @@ func TestReadFullYamlBidderConfig(t *testing.T) { expectedBidderInfo := BidderInfos{ bidder: { - Disabled: false, Maintainer: &MaintainerInfo{ Email: "some-email@domain.com", }, @@ -987,17 +1688,41 @@ func TestReadFullYamlBidderConfig(t *testing.T) { MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative}, }, }, - Debug: &DebugInfo{Allow: true}, ModifyingVastXmlAllowed: true, - Syncer: &Syncer{ - Supports: []string{"iframe"}, + Debug: &DebugInfo{ + Allow: true, + }, + Experiment: BidderInfoExperiment{ + AdsCert: BidderAdsCert{ + Enabled: true, + }, }, - Experiment: BidderInfoExperiment{AdsCert: BidderAdsCert{Enabled: true}}, EndpointCompression: "GZIP", OpenRTB: &OpenRTBInfo{ - Version: "2.6", GPPSupported: true, + Version: "2.6", + }, + Disabled: false, + ExtraAdapterInfo: "extra-info", + AppSecret: "app-secret", + PlatformID: "123", + UserSyncURL: "user-url", + Syncer: &Syncer{ + Key: "foo", + IFrame: &SyncerEndpoint{ + URL: "user-url", + RedirectURL: "https://redirect/setuid/iframe", + ExternalURL: "https://iframe.host", + UserMacro: "UID", + }, + Supports: []string{"iframe"}, + }, + XAPI: AdapterXAPI{ + Username: "uname", + Password: "pwd", + Tracker: "tracker", }, + Endpoint: "https://endpoint.com", }, } assert.Equalf(t, expectedBidderInfo, actualBidderInfo, "Bidder info objects aren't matching") diff --git a/config/compression.go b/config/compression.go new file mode 100644 index 00000000000..db85202b4a8 --- /dev/null +++ b/config/compression.go @@ -0,0 +1,21 @@ +package config + +import "github.com/prebid/prebid-server/util/httputil" + +type Compression struct { + Request CompressionInfo `mapstructure:"request"` + Response CompressionInfo `mapstructure:"response"` +} + +// CompressionInfo defines what types of compression algorithms are supported. +type CompressionInfo struct { + GZIP bool `mapstructure:"enable_gzip"` +} + +func (cfg *CompressionInfo) IsSupported(contentEncoding httputil.ContentEncoding) bool { + switch contentEncoding.Normalize() { + case httputil.ContentEncodingGZIP: + return cfg.GZIP + } + return false +} diff --git a/config/compression_test.go b/config/compression_test.go new file mode 100644 index 00000000000..cd9048cd99e --- /dev/null +++ b/config/compression_test.go @@ -0,0 +1,47 @@ +package config + +import ( + "testing" + + "github.com/prebid/prebid-server/util/httputil" + "github.com/stretchr/testify/assert" +) + +func TestReqCompressionCfgIsSupported(t *testing.T) { + testCases := []struct { + description string + cfg CompressionInfo + contentEncoding httputil.ContentEncoding + wantSupported bool + }{ + { + description: "Compression type not supported", + cfg: CompressionInfo{ + GZIP: true, + }, + contentEncoding: httputil.ContentEncoding("invalid"), + wantSupported: false, + }, + { + description: "Compression type supported", + cfg: CompressionInfo{ + GZIP: true, + }, + contentEncoding: httputil.ContentEncodingGZIP, + wantSupported: true, + }, + { + description: "Compression not enabled", + cfg: CompressionInfo{ + GZIP: false, + }, + contentEncoding: httputil.ContentEncodingGZIP, + wantSupported: false, + }, + } + + for _, test := range testCases { + got := test.cfg.IsSupported(test.contentEncoding) + assert.Equal(t, got, test.wantSupported, test.description) + } +} diff --git a/config/config.go b/config/config.go index e1d270649a7..e564c34ae3a 100644 --- a/config/config.go +++ b/config/config.go @@ -11,25 +11,26 @@ import ( "time" "github.com/golang/glog" - "github.com/prebid/openrtb/v19/openrtb2" - "github.com/spf13/viper" - "github.com/prebid/go-gdpr/consentconstants" + "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/spf13/viper" ) // Configuration specifies the static application config. type Configuration struct { - ExternalURL string `mapstructure:"external_url"` - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - UnixSocketEnable bool `mapstructure:"unix_socket_enable"` - UnixSocketName string `mapstructure:"unix_socket_name"` - Client HTTPClient `mapstructure:"http_client"` - CacheClient HTTPClient `mapstructure:"http_client_cache"` - AdminPort int `mapstructure:"admin_port"` - EnableGzip bool `mapstructure:"enable_gzip"` + ExternalURL string `mapstructure:"external_url"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + UnixSocketEnable bool `mapstructure:"unix_socket_enable"` + UnixSocketName string `mapstructure:"unix_socket_name"` + Client HTTPClient `mapstructure:"http_client"` + CacheClient HTTPClient `mapstructure:"http_client_cache"` + AdminPort int `mapstructure:"admin_port"` + EnableGzip bool `mapstructure:"enable_gzip"` + Compression Compression `mapstructure:"compression"` // GarbageCollectorThreshold allocates virtual memory (in bytes) which is not used by PBS but // serves as a hack to trigger the garbage collector only when the heap reaches at least this size. // More info: https://github.com/golang/go/issues/48409 @@ -38,6 +39,7 @@ type Configuration struct { // If empty, it will return a 204 with no content. StatusResponse string `mapstructure:"status_response"` AuctionTimeouts AuctionTimeouts `mapstructure:"auction_timeouts_ms"` + TmaxAdjustments TmaxAdjustments `mapstructure:"tmax_adjustments"` CacheURL Cache `mapstructure:"cache"` ExtCacheURL ExternalCache `mapstructure:"external_cache"` RecaptchaSecret string `mapstructure:"recaptcha_secret"` @@ -138,16 +140,20 @@ func (cfg *Configuration) validate(v *viper.Viper) []error { if cfg.AccountDefaults.Disabled { glog.Warning(`With account_defaults.disabled=true, host-defined accounts must exist and have "disabled":false. All other requests will be rejected.`) } - if cfg.AccountDefaults.Events.Enabled { - glog.Warning(`account_defaults.events will currently not do anything as the feature is still under development. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates`) - } if cfg.PriceFloors.Enabled { glog.Warning(`cfg.PriceFloors.Enabled will currently not do anything as price floors feature is still under development.`) } + if len(cfg.AccountDefaults.Events.VASTEvents) > 0 { + errs = append(errs, errors.New("account_defaults.Events.VASTEvents has no effect as the feature is under development.")) + } + errs = cfg.Experiment.validate(errs) errs = cfg.BidderInfos.validate(errs) + errs = cfg.AccountDefaults.Privacy.IPv6Config.Validate(errs) + errs = cfg.AccountDefaults.Privacy.IPv4Config.Validate(errs) + return errs } @@ -343,7 +349,7 @@ func (t *TCF2) IsEnabled() bool { // PurposeEnforced checks if full enforcement is turned on for a given purpose. With full enforcement enabled, the // GDPR full enforcement algorithm will execute for that purpose determining legal basis; otherwise it's skipped. -func (t *TCF2) PurposeEnforced(purpose consentconstants.Purpose) (value bool) { +func (t *TCF2) PurposeEnforced(purpose consentconstants.Purpose) (enforce bool) { if t.PurposeConfigs[purpose] == nil { return false } @@ -351,7 +357,7 @@ func (t *TCF2) PurposeEnforced(purpose consentconstants.Purpose) (value bool) { } // PurposeEnforcementAlgo returns the default enforcement algorithm for a given purpose -func (t *TCF2) PurposeEnforcementAlgo(purpose consentconstants.Purpose) (value TCF2EnforcementAlgo) { +func (t *TCF2) PurposeEnforcementAlgo(purpose consentconstants.Purpose) (enforcement TCF2EnforcementAlgo) { if c, exists := t.PurposeConfigs[purpose]; exists { return c.EnforceAlgoID } @@ -360,7 +366,7 @@ func (t *TCF2) PurposeEnforcementAlgo(purpose consentconstants.Purpose) (value T // PurposeEnforcingVendors checks if enforcing vendors is turned on for a given purpose. With enforcing vendors // enabled, the GDPR full enforcement algorithm considers the GVL when determining legal basis; otherwise it's skipped. -func (t *TCF2) PurposeEnforcingVendors(purpose consentconstants.Purpose) (value bool) { +func (t *TCF2) PurposeEnforcingVendors(purpose consentconstants.Purpose) (enforce bool) { if t.PurposeConfigs[purpose] == nil { return false } @@ -369,7 +375,7 @@ func (t *TCF2) PurposeEnforcingVendors(purpose consentconstants.Purpose) (value // PurposeVendorExceptions returns the vendor exception map for a given purpose if it exists, otherwise it returns // an empty map of vendor exceptions -func (t *TCF2) PurposeVendorExceptions(purpose consentconstants.Purpose) (value map[openrtb_ext.BidderName]struct{}) { +func (t *TCF2) PurposeVendorExceptions(purpose consentconstants.Purpose) (vendorExceptions map[openrtb_ext.BidderName]struct{}) { c, exists := t.PurposeConfigs[purpose] if exists && c.VendorExceptionMap != nil { @@ -380,13 +386,13 @@ func (t *TCF2) PurposeVendorExceptions(purpose consentconstants.Purpose) (value // FeatureOneEnforced checks if special feature one is enforced. If it is enforced, PBS will determine whether geo // information may be passed through in the bid request. -func (t *TCF2) FeatureOneEnforced() (value bool) { +func (t *TCF2) FeatureOneEnforced() bool { return t.SpecialFeature1.Enforce } // FeatureOneVendorException checks if the specified bidder is considered a vendor exception for special feature one. // If a bidder is a vendor exception, PBS will bypass the pass geo calculation passing the geo information in the bid request. -func (t *TCF2) FeatureOneVendorException(bidder openrtb_ext.BidderName) (value bool) { +func (t *TCF2) FeatureOneVendorException(bidder openrtb_ext.BidderName) bool { if _, ok := t.SpecialFeature1.VendorExceptionMap[bidder]; ok { return true } @@ -394,12 +400,12 @@ func (t *TCF2) FeatureOneVendorException(bidder openrtb_ext.BidderName) (value b } // PurposeOneTreatmentEnabled checks if purpose one treatment is enabled. -func (t *TCF2) PurposeOneTreatmentEnabled() (value bool) { +func (t *TCF2) PurposeOneTreatmentEnabled() bool { return t.PurposeOneTreatment.Enabled } // PurposeOneTreatmentAccessAllowed checks if purpose one treatment access is allowed. -func (t *TCF2) PurposeOneTreatmentAccessAllowed() (value bool) { +func (t *TCF2) PurposeOneTreatmentAccessAllowed() bool { return t.PurposeOneTreatment.AccessAllowed } @@ -688,6 +694,10 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin // Update account defaults and generate base json for patch c.AccountDefaults.CacheTTL = c.CacheURL.DefaultTTLs // comment this out to set explicitly in config + + // Update the deprecated and new events enabled values for account defaults. + c.AccountDefaults.EventsEnabled, c.AccountDefaults.Events.Enabled = migrateConfigEventsEnabled(c.AccountDefaults.EventsEnabled, c.AccountDefaults.Events.Enabled) + if err := c.MarshalAccountDefaults(); err != nil { return nil, err } @@ -825,7 +835,6 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("unix_socket_enable", false) // boolean which decide if the socket-server will be started. v.SetDefault("unix_socket_name", "prebid-server.sock") // path of the socket's file which must be listened. v.SetDefault("admin_port", 6060) - v.SetDefault("enable_gzip", false) v.SetDefault("garbage_collector_threshold", 0) v.SetDefault("status_response", "") v.SetDefault("datacenter", "") @@ -1008,6 +1017,12 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("account_defaults.price_floors.use_dynamic_data", false) v.SetDefault("account_defaults.price_floors.max_rules", 100) v.SetDefault("account_defaults.price_floors.max_schema_dims", 3) + v.SetDefault("account_defaults.events_enabled", false) + v.SetDefault("account_defaults.privacy.ipv6.anon_keep_bits", 56) + v.SetDefault("account_defaults.privacy.ipv4.anon_keep_bits", 24) + + v.SetDefault("compression.response.enable_gzip", false) + v.SetDefault("compression.request.enable_gzip", false) v.SetDefault("certificates_file", "") v.SetDefault("auto_gen_source_tid", true) @@ -1022,6 +1037,11 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("debug.timeout_notification.fail_only", false) v.SetDefault("debug.override_token", "") + v.SetDefault("tmax_adjustments.enabled", false) + v.SetDefault("tmax_adjustments.bidder_response_duration_min_ms", 0) + v.SetDefault("tmax_adjustments.bidder_network_latency_buffer_ms", 0) + v.SetDefault("tmax_adjustments.pbs_response_preparation_duration_ms", 0) + /* IPv4 /* Site Local: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 /* Link Local: 169.254.0.0/16 @@ -1052,6 +1072,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { migrateConfigSpecialFeature1(v) migrateConfigTCF2PurposeFlags(v) migrateConfigDatabaseConnection(v) + migrateConfigCompression(v) // These defaults must be set after the migrate functions because those functions look for the presence of these // config fields and there isn't a way to detect presence of a config field using the viper package if a default @@ -1092,6 +1113,8 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("gdpr.tcf2.special_feature1.vendor_exceptions", []openrtb_ext.BidderName{}) v.SetDefault("price_floors.enabled", false) + v.SetDefault("enable_gzip", false) + // Defaults for account_defaults.events.default_url v.SetDefault("account_defaults.events.default_url", "https://PBS_HOST/event?t=##PBS-EVENTTYPE##&vtype=##PBS-VASTEVENT##&b=##PBS-BIDID##&f=i&a=##PBS-ACCOUNTID##&ts=##PBS-TIMESTAMP##&bidder=##PBS-BIDDER##&int=##PBS-INTEGRATION##&mt=##PBS-MEDIATYPE##&ch=##PBS-CHANNEL##&aid=##PBS-AUCTIONID##&l=##PBS-LINEID##") @@ -1123,6 +1146,20 @@ func migrateConfig(v *viper.Viper) { } } +func migrateConfigCompression(v *viper.Viper) { + oldField := "enable_gzip" + newField := "compression.response.enable_gzip" + if v.IsSet(oldField) { + oldConfig := v.GetBool(oldField) + if v.IsSet(newField) { + glog.Warningf("using %s and ignoring deprecated %s", newField, oldField) + } else { + glog.Warningf("%s is deprecated and should be changed to %s", oldField, newField) + v.Set(newField, oldConfig) + } + } +} + func migrateConfigPurposeOneTreatment(v *viper.Viper) { if oldConfig, ok := v.Get("gdpr.tcf2.purpose_one_treatement").(map[string]interface{}); ok { if v.IsSet("gdpr.tcf2.purpose_one_treatment") { @@ -1371,6 +1408,28 @@ func migrateConfigDatabaseConnection(v *viper.Viper) { } } +// migrateConfigEventsEnabled is responsible for ensuring backward compatibility of events_enabled field. +// This function copies the value of newField "events.enabled" and set it to the oldField "events_enabled". +// This is necessary to achieve the desired order of precedence favoring the account values over the host values +// given the account fetcher JSON merge mechanics. +func migrateConfigEventsEnabled(oldFieldValue *bool, newFieldValue *bool) (updatedOldFieldValue, updatedNewFieldValue *bool) { + newField := "account_defaults.events.enabled" + oldField := "account_defaults.events_enabled" + + updatedOldFieldValue = oldFieldValue + if oldFieldValue != nil { + glog.Warningf("%s is deprecated and should be changed to %s", oldField, newField) + } + if newFieldValue != nil { + if oldFieldValue != nil { + glog.Warningf("using %s and ignoring deprecated %s", newField, oldField) + } + updatedOldFieldValue = ptrutil.ToPtr(*newFieldValue) + } + + return updatedOldFieldValue, nil +} + func isConfigInfoPresent(v *viper.Viper, prefix string, fields []string) bool { prefix = prefix + "." for _, field := range fields { @@ -1477,3 +1536,22 @@ func isValidCookieSize(maxCookieSize int) error { } return nil } + +// Tmax Adjustments enables PBS to estimate the tmax value for bidders, indicating the allotted time for them to respond to a request. +// It's important to note that the calculated tmax is just an estimate and will not be entirely precise. +// PBS will calculate the bidder tmax as follows: +// bidderTmax = request.tmax - reqProcessingTime - BidderNetworkLatencyBuffer - PBSResponsePreparationDuration +// Note that reqProcessingTime is time taken by PBS to process a given request before it is sent to bid adapters and is computed at run time. +type TmaxAdjustments struct { + // Enabled indicates whether bidder tmax should be calculated and passed on to bid adapters + Enabled bool `mapstructure:"enabled"` + // BidderNetworkLatencyBuffer accounts for network delays between PBS and bidder servers. + // A value of 0 indicates no network latency buffer should be accounted for when calculating the bidder tmax. + BidderNetworkLatencyBuffer uint `mapstructure:"bidder_network_latency_buffer_ms"` + // PBSResponsePreparationDuration accounts for amount of time required for PBS to process all bidder responses and generate final response for a request. + // A value of 0 indicates PBS response preparation time shouldn't be accounted for when calculating bidder tmax. + PBSResponsePreparationDuration uint `mapstructure:"pbs_response_preparation_duration_ms"` + // BidderResponseDurationMin is the minimum amount of time expected to get a response from a bidder request. + // PBS won't send a request to the bidder if the bidder tmax calculated is less than the BidderResponseDurationMin value + BidderResponseDurationMin uint `mapstructure:"bidder_response_duration_min_ms"` +} diff --git a/config/config_test.go b/config/config_test.go index d63dab8d6bb..057ed06ecc4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -12,6 +12,7 @@ import ( "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) @@ -145,53 +146,68 @@ func TestExternalCacheURLValidate(t *testing.T) { func TestDefaults(t *testing.T) { cfg, _ := newDefaultConfig(t) - cmpInts(t, "port", cfg.Port, 8000) - cmpInts(t, "admin_port", cfg.AdminPort, 6060) - cmpInts(t, "auction_timeouts_ms.max", int(cfg.AuctionTimeouts.Max), 0) - cmpInts(t, "max_request_size", int(cfg.MaxRequestSize), 1024*256) - cmpInts(t, "host_cookie.ttl_days", int(cfg.HostCookie.TTL), 90) - cmpInts(t, "host_cookie.max_cookie_size_bytes", cfg.HostCookie.MaxCookieSizeBytes, 0) - cmpInts(t, "currency_converter.fetch_interval_seconds", cfg.CurrencyConverter.FetchIntervalSeconds, 1800) - cmpStrings(t, "currency_converter.fetch_url", cfg.CurrencyConverter.FetchURL, "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json") - cmpBools(t, "account_required", cfg.AccountRequired, false) - cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20) - cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false) - cmpBools(t, "account_debug", cfg.Metrics.Disabled.AccountDebug, true) - cmpBools(t, "account_stored_responses", cfg.Metrics.Disabled.AccountStoredResponses, true) - cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true) - cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, false) - cmpStrings(t, "certificates_file", cfg.PemCertsFile, "") + cmpInts(t, "port", 8000, cfg.Port) + cmpInts(t, "admin_port", 6060, cfg.AdminPort) + cmpInts(t, "auction_timeouts_ms.max", 0, int(cfg.AuctionTimeouts.Max)) + cmpInts(t, "max_request_size", 1024*256, int(cfg.MaxRequestSize)) + cmpInts(t, "host_cookie.ttl_days", 90, int(cfg.HostCookie.TTL)) + cmpInts(t, "host_cookie.max_cookie_size_bytes", 0, cfg.HostCookie.MaxCookieSizeBytes) + cmpInts(t, "currency_converter.fetch_interval_seconds", 1800, cfg.CurrencyConverter.FetchIntervalSeconds) + cmpStrings(t, "currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json", cfg.CurrencyConverter.FetchURL) + cmpBools(t, "account_required", false, cfg.AccountRequired) + cmpInts(t, "metrics.influxdb.collection_rate_seconds", 20, cfg.Metrics.Influxdb.MetricSendInterval) + cmpBools(t, "account_adapter_details", false, cfg.Metrics.Disabled.AccountAdapterDetails) + cmpBools(t, "account_debug", true, cfg.Metrics.Disabled.AccountDebug) + cmpBools(t, "account_stored_responses", true, cfg.Metrics.Disabled.AccountStoredResponses) + cmpBools(t, "adapter_connections_metrics", true, cfg.Metrics.Disabled.AdapterConnectionMetrics) + cmpBools(t, "adapter_gdpr_request_blocked", false, cfg.Metrics.Disabled.AdapterGDPRRequestBlocked) + cmpStrings(t, "certificates_file", "", cfg.PemCertsFile) cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled) cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path) - cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, true) - cmpBools(t, "generate_bid_id", cfg.GenerateBidID, false) - cmpStrings(t, "experiment.adscert.mode", cfg.Experiment.AdCerts.Mode, "off") - cmpStrings(t, "experiment.adscert.inprocess.origin", cfg.Experiment.AdCerts.InProcess.Origin, "") - cmpStrings(t, "experiment.adscert.inprocess.key", cfg.Experiment.AdCerts.InProcess.PrivateKey, "") - cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds, 30) - cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds, 30) - cmpStrings(t, "experiment.adscert.remote.url", cfg.Experiment.AdCerts.Remote.Url, "") - cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", cfg.Experiment.AdCerts.Remote.SigningTimeoutMs, 5) + cmpBools(t, "auto_gen_source_tid", true, cfg.AutoGenSourceTID) + cmpBools(t, "generate_bid_id", false, cfg.GenerateBidID) + cmpStrings(t, "experiment.adscert.mode", "off", cfg.Experiment.AdCerts.Mode) + cmpStrings(t, "experiment.adscert.inprocess.origin", "", cfg.Experiment.AdCerts.InProcess.Origin) + cmpStrings(t, "experiment.adscert.inprocess.key", "", cfg.Experiment.AdCerts.InProcess.PrivateKey) + cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", 30, cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds) + cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", 30, cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds) + cmpStrings(t, "experiment.adscert.remote.url", "", cfg.Experiment.AdCerts.Remote.Url) + cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", 5, cfg.Experiment.AdCerts.Remote.SigningTimeoutMs) cmpNils(t, "host_schain_node", cfg.HostSChainNode) - cmpStrings(t, "datacenter", cfg.DataCenter, "") + cmpStrings(t, "datacenter", "", cfg.DataCenter) //Assert the price floor default values - cmpBools(t, "price_floors.enabled", cfg.PriceFloors.Enabled, false) - - cmpBools(t, "account_defaults.price_floors.enabled", cfg.AccountDefaults.PriceFloors.Enabled, false) - cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", cfg.AccountDefaults.PriceFloors.EnforceFloorsRate, 100) - cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment, true) - cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", cfg.AccountDefaults.PriceFloors.EnforceDealFloors, false) - cmpBools(t, "account_defaults.price_floors.use_dynamic_data", cfg.AccountDefaults.PriceFloors.UseDynamicData, false) - cmpInts(t, "account_defaults.price_floors.max_rules", cfg.AccountDefaults.PriceFloors.MaxRule, 100) - cmpInts(t, "account_defaults.price_floors.max_schema_dims", cfg.AccountDefaults.PriceFloors.MaxSchemaDims, 3) - - cmpBools(t, "hooks.enabled", cfg.Hooks.Enabled, false) - cmpStrings(t, "validations.banner_creative_max_size", cfg.Validations.BannerCreativeMaxSize, "skip") - cmpStrings(t, "validations.secure_markup", cfg.Validations.SecureMarkup, "skip") - cmpInts(t, "validations.max_creative_width", int(cfg.Validations.MaxCreativeWidth), 0) - cmpInts(t, "validations.max_creative_height", int(cfg.Validations.MaxCreativeHeight), 0) - cmpBools(t, "account_modules_metrics", cfg.Metrics.Disabled.AccountModulesMetrics, false) + cmpBools(t, "price_floors.enabled", false, cfg.PriceFloors.Enabled) + + // Assert compression related defaults + cmpBools(t, "enable_gzip", false, cfg.EnableGzip) + cmpBools(t, "compression.request.enable_gzip", false, cfg.Compression.Request.GZIP) + cmpBools(t, "compression.response.enable_gzip", false, cfg.Compression.Response.GZIP) + + cmpBools(t, "account_defaults.price_floors.enabled", false, cfg.AccountDefaults.PriceFloors.Enabled) + cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", 100, cfg.AccountDefaults.PriceFloors.EnforceFloorsRate) + cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", true, cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment) + cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", false, cfg.AccountDefaults.PriceFloors.EnforceDealFloors) + cmpBools(t, "account_defaults.price_floors.use_dynamic_data", false, cfg.AccountDefaults.PriceFloors.UseDynamicData) + cmpInts(t, "account_defaults.price_floors.max_rules", 100, cfg.AccountDefaults.PriceFloors.MaxRule) + cmpInts(t, "account_defaults.price_floors.max_schema_dims", 3, cfg.AccountDefaults.PriceFloors.MaxSchemaDims) + cmpBools(t, "account_defaults.events_enabled", *cfg.AccountDefaults.EventsEnabled, false) + cmpNils(t, "account_defaults.events.enabled", cfg.AccountDefaults.Events.Enabled) + + cmpBools(t, "hooks.enabled", false, cfg.Hooks.Enabled) + cmpStrings(t, "validations.banner_creative_max_size", "skip", cfg.Validations.BannerCreativeMaxSize) + cmpStrings(t, "validations.secure_markup", "skip", cfg.Validations.SecureMarkup) + cmpInts(t, "validations.max_creative_width", 0, int(cfg.Validations.MaxCreativeWidth)) + cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight)) + cmpBools(t, "account_modules_metrics", false, cfg.Metrics.Disabled.AccountModulesMetrics) + + cmpBools(t, "tmax_adjustments.enabled", false, cfg.TmaxAdjustments.Enabled) + cmpUnsignedInts(t, "tmax_adjustments.bidder_response_duration_min_ms", 0, cfg.TmaxAdjustments.BidderResponseDurationMin) + cmpUnsignedInts(t, "tmax_adjustments.bidder_network_latency_buffer_ms", 0, cfg.TmaxAdjustments.BidderNetworkLatencyBuffer) + cmpUnsignedInts(t, "tmax_adjustments.pbs_response_preparation_duration_ms", 0, cfg.TmaxAdjustments.PBSResponsePreparationDuration) + + cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 56, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits) + cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 24, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) //Assert purpose VendorExceptionMap hash tables were built correctly expectedTCF2 := TCF2{ @@ -311,6 +327,7 @@ func TestDefaults(t *testing.T) { assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2") } +// When adding a new field, make sure the indentations are spaces not tabs otherwise read config may fail to parse the new field value. var fullConfig = []byte(` gdpr: host_vendor_id: 15 @@ -369,6 +386,12 @@ external_url: http://prebid-server.prebid.org/ host: prebid-server.prebid.org port: 1234 admin_port: 5678 +enable_gzip: false +compression: + request: + enable_gzip: true + response: + enable_gzip: false garbage_collector_threshold: 1 datacenter: "1" auction_timeouts_ms: @@ -446,6 +469,9 @@ hooks: price_floors: enabled: true account_defaults: + events_enabled: false + events: + enabled: true price_floors: enabled: true enforce_floors_rate: 50 @@ -454,6 +480,16 @@ account_defaults: use_dynamic_data: true max_rules: 120 max_schema_dims: 5 + privacy: + ipv6: + anon_keep_bits: 50 + ipv4: + anon_keep_bits: 20 +tmax_adjustments: + enabled: true + bidder_response_duration_min_ms: 700 + bidder_network_latency_buffer_ms: 100 + pbs_response_preparation_duration_ms: 100 `) var oldStoredRequestsConfig = []byte(` @@ -462,24 +498,29 @@ stored_requests: directorypath: "/somepath" `) -func cmpStrings(t *testing.T, key string, a string, b string) { +func cmpStrings(t *testing.T, key, expected, actual string) { + t.Helper() + assert.Equal(t, expected, actual, "%s: %s != %s", key, expected, actual) +} + +func cmpInts(t *testing.T, key string, expected, actual int) { t.Helper() - assert.Equal(t, a, b, "%s: %s != %s", key, a, b) + assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual) } -func cmpInts(t *testing.T, key string, a int, b int) { +func cmpUnsignedInts(t *testing.T, key string, expected, actual uint) { t.Helper() - assert.Equal(t, a, b, "%s: %d != %d", key, a, b) + assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual) } -func cmpInt8s(t *testing.T, key string, a *int8, b *int8) { +func cmpInt8s(t *testing.T, key string, expected, actual *int8) { t.Helper() - assert.Equal(t, a, b, "%s: %d != %d", key, a, b) + assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual) } -func cmpBools(t *testing.T, key string, a bool, b bool) { +func cmpBools(t *testing.T, key string, expected, actual bool) { t.Helper() - assert.Equal(t, a, b, "%s: %t != %t", key, a, b) + assert.Equal(t, expected, actual, "%s: %t != %t", key, expected, actual) } func cmpNils(t *testing.T, key string, a interface{}) { @@ -496,53 +537,67 @@ func TestFullConfig(t *testing.T) { v.ReadConfig(bytes.NewBuffer(fullConfig)) cfg, err := New(v, bidderInfos, mockNormalizeBidderName) assert.NoError(t, err, "Setting up config should work but it doesn't") - cmpStrings(t, "cookie domain", cfg.HostCookie.Domain, "cookies.prebid.org") - cmpStrings(t, "cookie name", cfg.HostCookie.CookieName, "userid") - cmpStrings(t, "cookie family", cfg.HostCookie.Family, "prebid") - cmpStrings(t, "opt out", cfg.HostCookie.OptOutURL, "http://prebid.org/optout") - cmpStrings(t, "opt in", cfg.HostCookie.OptInURL, "http://prebid.org/optin") - cmpStrings(t, "external url", cfg.ExternalURL, "http://prebid-server.prebid.org/") - cmpStrings(t, "host", cfg.Host, "prebid-server.prebid.org") - cmpInts(t, "port", cfg.Port, 1234) - cmpInts(t, "admin_port", cfg.AdminPort, 5678) - cmpInts(t, "garbage_collector_threshold", cfg.GarbageCollectorThreshold, 1) - cmpInts(t, "auction_timeouts_ms.default", int(cfg.AuctionTimeouts.Default), 50) - cmpInts(t, "auction_timeouts_ms.max", int(cfg.AuctionTimeouts.Max), 123) - cmpStrings(t, "cache.scheme", cfg.CacheURL.Scheme, "http") - cmpStrings(t, "cache.host", cfg.CacheURL.Host, "prebidcache.net") - cmpStrings(t, "cache.query", cfg.CacheURL.Query, "uuid=%PBS_CACHE_UUID%") - cmpStrings(t, "external_cache.scheme", cfg.ExtCacheURL.Scheme, "https") - cmpStrings(t, "external_cache.host", cfg.ExtCacheURL.Host, "www.externalprebidcache.net") - cmpStrings(t, "external_cache.path", cfg.ExtCacheURL.Path, "/endpoints/cache") - cmpInts(t, "http_client.max_connections_per_host", cfg.Client.MaxConnsPerHost, 10) - cmpInts(t, "http_client.max_idle_connections", cfg.Client.MaxIdleConns, 500) - cmpInts(t, "http_client.max_idle_connections_per_host", cfg.Client.MaxIdleConnsPerHost, 20) - cmpInts(t, "http_client.idle_connection_timeout_seconds", cfg.Client.IdleConnTimeout, 30) - cmpInts(t, "http_client_cache.max_connections_per_host", cfg.CacheClient.MaxConnsPerHost, 5) - cmpInts(t, "http_client_cache.max_idle_connections", cfg.CacheClient.MaxIdleConns, 1) - cmpInts(t, "http_client_cache.max_idle_connections_per_host", cfg.CacheClient.MaxIdleConnsPerHost, 2) - cmpInts(t, "http_client_cache.idle_connection_timeout_seconds", cfg.CacheClient.IdleConnTimeout, 3) - cmpInts(t, "gdpr.host_vendor_id", cfg.GDPR.HostVendorID, 15) - cmpStrings(t, "gdpr.default_value", cfg.GDPR.DefaultValue, "1") - cmpStrings(t, "host_schain_node.asi", cfg.HostSChainNode.ASI, "pbshostcompany.com") - cmpStrings(t, "host_schain_node.sid", cfg.HostSChainNode.SID, "00001") - cmpStrings(t, "host_schain_node.rid", cfg.HostSChainNode.RID, "BidRequest") - cmpInt8s(t, "host_schain_node.hp", cfg.HostSChainNode.HP, &int8One) - cmpStrings(t, "datacenter", cfg.DataCenter, "1") - cmpStrings(t, "validations.banner_creative_max_size", cfg.Validations.BannerCreativeMaxSize, "skip") - cmpStrings(t, "validations.secure_markup", cfg.Validations.SecureMarkup, "skip") - cmpInts(t, "validations.max_creative_width", int(cfg.Validations.MaxCreativeWidth), 0) - cmpInts(t, "validations.max_creative_height", int(cfg.Validations.MaxCreativeHeight), 0) + cmpStrings(t, "cookie domain", "cookies.prebid.org", cfg.HostCookie.Domain) + cmpStrings(t, "cookie name", "userid", cfg.HostCookie.CookieName) + cmpStrings(t, "cookie family", "prebid", cfg.HostCookie.Family) + cmpStrings(t, "opt out", "http://prebid.org/optout", cfg.HostCookie.OptOutURL) + cmpStrings(t, "opt in", "http://prebid.org/optin", cfg.HostCookie.OptInURL) + cmpStrings(t, "external url", "http://prebid-server.prebid.org/", cfg.ExternalURL) + cmpStrings(t, "host", "prebid-server.prebid.org", cfg.Host) + cmpInts(t, "port", 1234, cfg.Port) + cmpInts(t, "admin_port", 5678, cfg.AdminPort) + cmpInts(t, "garbage_collector_threshold", 1, cfg.GarbageCollectorThreshold) + cmpInts(t, "auction_timeouts_ms.default", 50, int(cfg.AuctionTimeouts.Default)) + cmpInts(t, "auction_timeouts_ms.max", 123, int(cfg.AuctionTimeouts.Max)) + cmpStrings(t, "cache.scheme", "http", cfg.CacheURL.Scheme) + cmpStrings(t, "cache.host", "prebidcache.net", cfg.CacheURL.Host) + cmpStrings(t, "cache.query", "uuid=%PBS_CACHE_UUID%", cfg.CacheURL.Query) + cmpStrings(t, "external_cache.scheme", "https", cfg.ExtCacheURL.Scheme) + cmpStrings(t, "external_cache.host", "www.externalprebidcache.net", cfg.ExtCacheURL.Host) + cmpStrings(t, "external_cache.path", "/endpoints/cache", cfg.ExtCacheURL.Path) + cmpInts(t, "http_client.max_connections_per_host", 10, cfg.Client.MaxConnsPerHost) + cmpInts(t, "http_client.max_idle_connections", 500, cfg.Client.MaxIdleConns) + cmpInts(t, "http_client.max_idle_connections_per_host", 20, cfg.Client.MaxIdleConnsPerHost) + cmpInts(t, "http_client.idle_connection_timeout_seconds", 30, cfg.Client.IdleConnTimeout) + cmpInts(t, "http_client_cache.max_connections_per_host", 5, cfg.CacheClient.MaxConnsPerHost) + cmpInts(t, "http_client_cache.max_idle_connections", 1, cfg.CacheClient.MaxIdleConns) + cmpInts(t, "http_client_cache.max_idle_connections_per_host", 2, cfg.CacheClient.MaxIdleConnsPerHost) + cmpInts(t, "http_client_cache.idle_connection_timeout_seconds", 3, cfg.CacheClient.IdleConnTimeout) + cmpInts(t, "gdpr.host_vendor_id", 15, cfg.GDPR.HostVendorID) + cmpStrings(t, "gdpr.default_value", "1", cfg.GDPR.DefaultValue) + cmpStrings(t, "host_schain_node.asi", "pbshostcompany.com", cfg.HostSChainNode.ASI) + cmpStrings(t, "host_schain_node.sid", "00001", cfg.HostSChainNode.SID) + cmpStrings(t, "host_schain_node.rid", "BidRequest", cfg.HostSChainNode.RID) + cmpInt8s(t, "host_schain_node.hp", &int8One, cfg.HostSChainNode.HP) + cmpStrings(t, "datacenter", "1", cfg.DataCenter) + cmpStrings(t, "validations.banner_creative_max_size", "skip", cfg.Validations.BannerCreativeMaxSize) + cmpStrings(t, "validations.secure_markup", "skip", cfg.Validations.SecureMarkup) + cmpInts(t, "validations.max_creative_width", 0, int(cfg.Validations.MaxCreativeWidth)) + cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight)) + cmpBools(t, "tmax_adjustments.enabled", true, cfg.TmaxAdjustments.Enabled) + cmpUnsignedInts(t, "tmax_adjustments.bidder_response_duration_min_ms", 700, cfg.TmaxAdjustments.BidderResponseDurationMin) + cmpUnsignedInts(t, "tmax_adjustments.bidder_network_latency_buffer_ms", 100, cfg.TmaxAdjustments.BidderNetworkLatencyBuffer) + cmpUnsignedInts(t, "tmax_adjustments.pbs_response_preparation_duration_ms", 100, cfg.TmaxAdjustments.PBSResponsePreparationDuration) //Assert the price floor values - cmpBools(t, "price_floors.enabled", cfg.PriceFloors.Enabled, true) - cmpBools(t, "account_defaults.price_floors.enabled", cfg.AccountDefaults.PriceFloors.Enabled, true) - cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", cfg.AccountDefaults.PriceFloors.EnforceFloorsRate, 50) - cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment, false) - cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", cfg.AccountDefaults.PriceFloors.EnforceDealFloors, true) - cmpBools(t, "account_defaults.price_floors.use_dynamic_data", cfg.AccountDefaults.PriceFloors.UseDynamicData, true) - cmpInts(t, "account_defaults.price_floors.max_rules", cfg.AccountDefaults.PriceFloors.MaxRule, 120) - cmpInts(t, "account_defaults.price_floors.max_schema_dims", cfg.AccountDefaults.PriceFloors.MaxSchemaDims, 5) + cmpBools(t, "price_floors.enabled", true, cfg.PriceFloors.Enabled) + cmpBools(t, "account_defaults.price_floors.enabled", true, cfg.AccountDefaults.PriceFloors.Enabled) + cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", 50, cfg.AccountDefaults.PriceFloors.EnforceFloorsRate) + cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", false, cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment) + cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", true, cfg.AccountDefaults.PriceFloors.EnforceDealFloors) + cmpBools(t, "account_defaults.price_floors.use_dynamic_data", true, cfg.AccountDefaults.PriceFloors.UseDynamicData) + cmpInts(t, "account_defaults.price_floors.max_rules", 120, cfg.AccountDefaults.PriceFloors.MaxRule) + cmpInts(t, "account_defaults.price_floors.max_schema_dims", 5, cfg.AccountDefaults.PriceFloors.MaxSchemaDims) + cmpBools(t, "account_defaults.events_enabled", *cfg.AccountDefaults.EventsEnabled, true) + cmpNils(t, "account_defaults.events.enabled", cfg.AccountDefaults.Events.Enabled) + + cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits) + cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 20, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) + + // Assert compression related defaults + cmpBools(t, "enable_gzip", false, cfg.EnableGzip) + cmpBools(t, "compression.request.enable_gzip", true, cfg.Compression.Request.GZIP) + cmpBools(t, "compression.response.enable_gzip", false, cfg.Compression.Response.GZIP) //Assert the NonStandardPublishers was correctly unmarshalled assert.Equal(t, []string{"pub1", "pub2"}, cfg.GDPR.NonStandardPublishers, "gdpr.non_standard_publishers") @@ -552,16 +607,16 @@ func TestFullConfig(t *testing.T) { assert.Equal(t, []string{"eea1", "eea2"}, cfg.GDPR.EEACountries, "gdpr.eea_countries") assert.Equal(t, map[string]struct{}{"eea1": {}, "eea2": {}}, cfg.GDPR.EEACountriesMap, "gdpr.eea_countries Hash Map") - cmpBools(t, "ccpa.enforce", cfg.CCPA.Enforce, true) - cmpBools(t, "lmt.enforce", cfg.LMT.Enforce, true) + cmpBools(t, "ccpa.enforce", true, cfg.CCPA.Enforce) + cmpBools(t, "lmt.enforce", true, cfg.LMT.Enforce) //Assert the NonStandardPublishers was correctly unmarshalled - cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[0], "spamAppID") - cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[1], "sketchy-app-id") + cmpStrings(t, "blacklisted_apps", "spamAppID", cfg.BlacklistedApps[0]) + cmpStrings(t, "blacklisted_apps", "sketchy-app-id", cfg.BlacklistedApps[1]) //Assert the BlacklistedAppMap hash table was built correctly for i := 0; i < len(cfg.BlacklistedApps); i++ { - cmpBools(t, "cfg.BlacklistedAppMap", cfg.BlacklistedAppMap[cfg.BlacklistedApps[i]], true) + cmpBools(t, "cfg.BlacklistedAppMap", true, cfg.BlacklistedAppMap[cfg.BlacklistedApps[i]]) } //Assert purpose VendorExceptionMap hash tables were built correctly @@ -681,40 +736,40 @@ func TestFullConfig(t *testing.T) { } assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2") - cmpStrings(t, "currency_converter.fetch_url", cfg.CurrencyConverter.FetchURL, "https://currency.prebid.org") - cmpInts(t, "currency_converter.fetch_interval_seconds", cfg.CurrencyConverter.FetchIntervalSeconds, 1800) - cmpStrings(t, "recaptcha_secret", cfg.RecaptchaSecret, "asdfasdfasdfasdf") - cmpStrings(t, "metrics.influxdb.host", cfg.Metrics.Influxdb.Host, "upstream:8232") - cmpStrings(t, "metrics.influxdb.database", cfg.Metrics.Influxdb.Database, "metricsdb") - cmpStrings(t, "metrics.influxdb.measurement", cfg.Metrics.Influxdb.Measurement, "anyMeasurement") - cmpStrings(t, "metrics.influxdb.username", cfg.Metrics.Influxdb.Username, "admin") - cmpStrings(t, "metrics.influxdb.password", cfg.Metrics.Influxdb.Password, "admin1324") - cmpBools(t, "metrics.influxdb.align_timestamps", cfg.Metrics.Influxdb.AlignTimestamps, true) - cmpInts(t, "metrics.influxdb.metric_send_interval", cfg.Metrics.Influxdb.MetricSendInterval, 30) - cmpStrings(t, "", cfg.CacheURL.GetBaseURL(), "http://prebidcache.net") - cmpStrings(t, "", cfg.GetCachedAssetURL("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11"), "http://prebidcache.net/cache?uuid=a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11") - cmpBools(t, "account_required", cfg.AccountRequired, true) - cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, false) - cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true) - cmpBools(t, "account_debug", cfg.Metrics.Disabled.AccountDebug, false) - cmpBools(t, "account_stored_responses", cfg.Metrics.Disabled.AccountStoredResponses, false) - cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true) - cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, true) - cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem") - cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24") - cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16") - cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[1], "2222::/16") - cmpBools(t, "generate_bid_id", cfg.GenerateBidID, true) - cmpStrings(t, "debug.override_token", cfg.Debug.OverrideToken, "") - cmpStrings(t, "experiment.adscert.mode", cfg.Experiment.AdCerts.Mode, "inprocess") - cmpStrings(t, "experiment.adscert.inprocess.origin", cfg.Experiment.AdCerts.InProcess.Origin, "http://test.com") - cmpStrings(t, "experiment.adscert.inprocess.key", cfg.Experiment.AdCerts.InProcess.PrivateKey, "ABC123") - cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds, 40) - cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds, 60) - cmpStrings(t, "experiment.adscert.remote.url", cfg.Experiment.AdCerts.Remote.Url, "") - cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", cfg.Experiment.AdCerts.Remote.SigningTimeoutMs, 10) - cmpBools(t, "hooks.enabled", cfg.Hooks.Enabled, true) - cmpBools(t, "account_modules_metrics", cfg.Metrics.Disabled.AccountModulesMetrics, true) + cmpStrings(t, "currency_converter.fetch_url", "https://currency.prebid.org", cfg.CurrencyConverter.FetchURL) + cmpInts(t, "currency_converter.fetch_interval_seconds", 1800, cfg.CurrencyConverter.FetchIntervalSeconds) + cmpStrings(t, "recaptcha_secret", "asdfasdfasdfasdf", cfg.RecaptchaSecret) + cmpStrings(t, "metrics.influxdb.host", "upstream:8232", cfg.Metrics.Influxdb.Host) + cmpStrings(t, "metrics.influxdb.database", "metricsdb", cfg.Metrics.Influxdb.Database) + cmpStrings(t, "metrics.influxdb.measurement", "anyMeasurement", cfg.Metrics.Influxdb.Measurement) + cmpStrings(t, "metrics.influxdb.username", "admin", cfg.Metrics.Influxdb.Username) + cmpStrings(t, "metrics.influxdb.password", "admin1324", cfg.Metrics.Influxdb.Password) + cmpBools(t, "metrics.influxdb.align_timestamps", true, cfg.Metrics.Influxdb.AlignTimestamps) + cmpInts(t, "metrics.influxdb.metric_send_interval", 30, cfg.Metrics.Influxdb.MetricSendInterval) + cmpStrings(t, "", "http://prebidcache.net", cfg.CacheURL.GetBaseURL()) + cmpStrings(t, "", "http://prebidcache.net/cache?uuid=a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11", cfg.GetCachedAssetURL("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11")) + cmpBools(t, "account_required", true, cfg.AccountRequired) + cmpBools(t, "auto_gen_source_tid", false, cfg.AutoGenSourceTID) + cmpBools(t, "account_adapter_details", true, cfg.Metrics.Disabled.AccountAdapterDetails) + cmpBools(t, "account_debug", false, cfg.Metrics.Disabled.AccountDebug) + cmpBools(t, "account_stored_responses", false, cfg.Metrics.Disabled.AccountStoredResponses) + cmpBools(t, "adapter_connections_metrics", true, cfg.Metrics.Disabled.AdapterConnectionMetrics) + cmpBools(t, "adapter_gdpr_request_blocked", true, cfg.Metrics.Disabled.AdapterGDPRRequestBlocked) + cmpStrings(t, "certificates_file", "/etc/ssl/cert.pem", cfg.PemCertsFile) + cmpStrings(t, "request_validation.ipv4_private_networks", "1.1.1.0/24", cfg.RequestValidation.IPv4PrivateNetworks[0]) + cmpStrings(t, "request_validation.ipv6_private_networks", "1111::/16", cfg.RequestValidation.IPv6PrivateNetworks[0]) + cmpStrings(t, "request_validation.ipv6_private_networks", "2222::/16", cfg.RequestValidation.IPv6PrivateNetworks[1]) + cmpBools(t, "generate_bid_id", true, cfg.GenerateBidID) + cmpStrings(t, "debug.override_token", "", cfg.Debug.OverrideToken) + cmpStrings(t, "experiment.adscert.mode", "inprocess", cfg.Experiment.AdCerts.Mode) + cmpStrings(t, "experiment.adscert.inprocess.origin", "http://test.com", cfg.Experiment.AdCerts.InProcess.Origin) + cmpStrings(t, "experiment.adscert.inprocess.key", "ABC123", cfg.Experiment.AdCerts.InProcess.PrivateKey) + cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", 40, cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds) + cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", 60, cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds) + cmpStrings(t, "experiment.adscert.remote.url", "", cfg.Experiment.AdCerts.Remote.Url) + cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", 10, cfg.Experiment.AdCerts.Remote.SigningTimeoutMs) + cmpBools(t, "hooks.enabled", true, cfg.Hooks.Enabled) + cmpBools(t, "account_modules_metrics", true, cfg.Metrics.Disabled.AccountModulesMetrics) } func TestValidateConfig(t *testing.T) { @@ -841,7 +896,6 @@ func TestUserSyncFromEnv(t *testing.T) { assert.Nil(t, cfg.BidderInfos["bidder2"].Syncer.Redirect) assert.Nil(t, cfg.BidderInfos["bidder2"].Syncer.SupportCORS) - assert.Nil(t, cfg.BidderInfos["brightroll"].Syncer) } func TestBidderInfoFromEnv(t *testing.T) { @@ -2519,7 +2573,8 @@ func TestMigrateConfigDatabaseQueryParams(t *testing.T) { v := viper.New() v.SetConfigType("yaml") - v.ReadConfig(bytes.NewBuffer(config)) + err := v.ReadConfig(bytes.NewBuffer(config)) + assert.NoError(t, err) migrateConfigDatabaseConnection(v) @@ -2542,6 +2597,74 @@ func TestMigrateConfigDatabaseQueryParams(t *testing.T) { assert.Equal(t, want_queries.poll_for_updates_amp_query, v.GetString("stored_responses.database.poll_for_updates.amp_query")) } +func TestMigrateConfigCompression(t *testing.T) { + testCases := []struct { + desc string + config []byte + wantEnableGZIP bool + wantReqGZIPEnabled bool + wantRespGZIPEnabled bool + }{ + + { + desc: "New config and old config not set", + config: []byte{}, + wantEnableGZIP: false, + wantReqGZIPEnabled: false, + wantRespGZIPEnabled: false, + }, + { + desc: "Old config set, new config not set", + config: []byte(` + enable_gzip: true + `), + wantEnableGZIP: true, + wantRespGZIPEnabled: true, + wantReqGZIPEnabled: false, + }, + { + desc: "Old config not set, new config set", + config: []byte(` + compression: + response: + enable_gzip: true + request: + enable_gzip: false + `), + wantEnableGZIP: false, + wantRespGZIPEnabled: true, + wantReqGZIPEnabled: false, + }, + { + desc: "Old config set and new config set", + config: []byte(` + enable_gzip: true + compression: + response: + enable_gzip: false + request: + enable_gzip: true + `), + wantEnableGZIP: true, + wantRespGZIPEnabled: false, + wantReqGZIPEnabled: true, + }, + } + + for _, test := range testCases { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(bytes.NewBuffer(test.config)) + assert.NoError(t, err) + + migrateConfigCompression(v) + + assert.Equal(t, test.wantEnableGZIP, v.GetBool("enable_gzip"), test.desc) + assert.Equal(t, test.wantReqGZIPEnabled, v.GetBool("compression.request.enable_gzip"), test.desc) + assert.Equal(t, test.wantRespGZIPEnabled, v.GetBool("compression.response.enable_gzip"), test.desc) + } +} + func TestIsConfigInfoPresent(t *testing.T) { configPrefix1Field2Only := []byte(` prefix1: @@ -3191,3 +3314,52 @@ func TestTCF2FeatureOneVendorException(t *testing.T) { assert.Equal(t, tt.wantIsVendorException, value, tt.description) } } + +func TestMigrateConfigEventsEnabled(t *testing.T) { + testCases := []struct { + name string + oldFieldValue *bool + newFieldValue *bool + expectedOldFieldValue *bool + expectedNewFieldValue *bool + }{ + { + name: "Both old and new fields are nil", + oldFieldValue: nil, + newFieldValue: nil, + expectedOldFieldValue: nil, + expectedNewFieldValue: nil, + }, + { + name: "Only old field is set", + oldFieldValue: ptrutil.ToPtr(true), + newFieldValue: nil, + expectedOldFieldValue: ptrutil.ToPtr(true), + expectedNewFieldValue: nil, + }, + { + name: "Only new field is set", + oldFieldValue: nil, + newFieldValue: ptrutil.ToPtr(true), + expectedOldFieldValue: ptrutil.ToPtr(true), + expectedNewFieldValue: nil, + }, + { + name: "Both old and new fields are set, override old field with new field value", + oldFieldValue: ptrutil.ToPtr(false), + newFieldValue: ptrutil.ToPtr(true), + expectedOldFieldValue: ptrutil.ToPtr(true), + expectedNewFieldValue: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + updatedOldFieldValue, updatedNewFieldValue := migrateConfigEventsEnabled(tc.oldFieldValue, tc.newFieldValue) + + assert.Equal(t, tc.expectedOldFieldValue, updatedOldFieldValue) + assert.Nil(t, updatedNewFieldValue) + assert.Nil(t, tc.expectedNewFieldValue) + }) + } +} diff --git a/config/events.go b/config/events.go index cf3139d83ca..83d2df4b58d 100644 --- a/config/events.go +++ b/config/events.go @@ -61,14 +61,14 @@ type VASTEvent struct { // within the VAST XML // Don't enable this feature. It is still under developmment. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates type Events struct { - Enabled bool `mapstructure:"enabled" json:"enabled"` + Enabled *bool `mapstructure:"enabled" json:"enabled"` DefaultURL string `mapstructure:"default_url" json:"default_url"` VASTEvents []VASTEvent `mapstructure:"vast_events" json:"vast_events,omitempty"` } // validate verifies the events object and returns error if at least one is invalid. func (e Events) validate(errs []error) []error { - if e.Enabled { + if e.IsEnabled() { if !isValidURL(e.DefaultURL) { return append(errs, errors.New("Invalid events.default_url")) } @@ -147,3 +147,8 @@ func isValidURL(eventURL string) bool { func (e VASTEvent) isTrackingEvent() bool { return e.CreateElement == TrackingVASTElement } + +// IsEnabled function returns the value of events.enabled field +func (e Events) IsEnabled() bool { + return e.Enabled != nil && *e.Enabled +} diff --git a/config/events_test.go b/config/events_test.go index b4c196e9fa9..4baa9066dec 100644 --- a/config/events_test.go +++ b/config/events_test.go @@ -3,6 +3,7 @@ package config import ( "testing" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" ) @@ -261,22 +262,29 @@ func TestValidate(t *testing.T) { { description: "Empty default URL", events: Events{ - Enabled: true, + Enabled: ptrutil.ToPtr(true), }, expectErr: true, }, { description: "Events are disabled. Skips validations", events: Events{ - Enabled: false, + Enabled: ptrutil.ToPtr(false), DefaultURL: "", }, expectErr: false, }, + { + description: "Events are nil. Skip validations", + events: Events{ + Enabled: nil, + }, + expectErr: false, + }, { description: "No VAST Events and default URL present", events: Events{ - Enabled: true, + Enabled: ptrutil.ToPtr(true), DefaultURL: "http://prebid.org", }, expectErr: false, @@ -284,7 +292,7 @@ func TestValidate(t *testing.T) { { description: "Invalid VAST Event", events: Events{ - Enabled: true, + Enabled: ptrutil.ToPtr(true), DefaultURL: "http://prebid.org", VASTEvents: []VASTEvent{ {}, @@ -327,3 +335,34 @@ func TestValidateVASTEvents(t *testing.T) { assert.Equal(t, !test.expectErr, err == nil, test.description) } } + +func TestIsEnabled(t *testing.T) { + testCases := []struct { + name string + events Events + expected bool + }{ + { + name: "nil pointer", + events: Events{}, + expected: false, + }, + { + name: "event false", + events: Events{Enabled: ptrutil.ToPtr(false)}, + expected: false, + }, + { + name: "event true", + events: Events{Enabled: ptrutil.ToPtr(true)}, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := tc.events.IsEnabled() + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index 4b043bddc41..50cb449193a 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -12,6 +12,8 @@ import ( "github.com/golang/glog" "github.com/julienschmidt/httprouter" + gpplib "github.com/prebid/go-gpp" + gppConstants "github.com/prebid/go-gpp/constants" accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" @@ -25,6 +27,7 @@ import ( gppPrivacy "github.com/prebid/prebid-server/privacy/gpp" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/usersync" + stringutil "github.com/prebid/prebid-server/util/stringutil" ) var ( @@ -87,8 +90,10 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht c.handleError(w, err, http.StatusBadRequest) return } + decoder := usersync.Base64Decoder{} - cookie := usersync.ParseCookieFromRequest(r, &c.config.HostCookie) + cookie := usersync.ReadCookie(r, decoder, &c.config.HostCookie) + usersync.SyncHostCookie(r, cookie, &c.config.HostCookie) result := c.chooser.Choose(request, cookie) switch result.Status { @@ -125,40 +130,12 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr return usersync.Request{}, privacy.Policies{}, combineErrors(fetchErrs) } - var gdprString string - if request.GDPR != nil { - gdprString = strconv.Itoa(*request.GDPR) - } - gdprSignal, err := gdpr.SignalParse(gdprString) - if err != nil { - return usersync.Request{}, privacy.Policies{}, err - } - - if request.GDPRConsent == "" { - if gdprSignal == gdpr.SignalYes { - return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissing - } - - if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, c.privacyConfig.gdprConfig.DefaultValue) == gdpr.SignalYes { - return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissingSignalAmbiguous - } - } - request = c.setLimit(request, account.CookieSync) request = c.setCooperativeSync(request, account.CookieSync) - privacyPolicies := privacy.Policies{ - GDPR: gdprPrivacy.Policy{ - Signal: gdprString, - Consent: request.GDPRConsent, - }, - CCPA: ccpa.Policy{ - Consent: request.USPrivacy, - }, - GPP: gppPrivacy.Policy{ - Consent: request.GPP, - RawSID: request.GPPSid, - }, + privacyPolicies, gdprSignal, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue) + if err != nil { + return usersync.Request{}, privacy.Policies{}, err } ccpaParsedPolicy := ccpa.ParsedPolicy{} @@ -172,13 +149,20 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr } } + activityControl, activitiesErr := privacy.NewActivityControl(&account.Privacy) + if activitiesErr != nil { + if errortypes.ContainsFatalError([]error{activitiesErr}) { + activityControl = privacy.ActivityControl{} + } + } + syncTypeFilter, err := parseTypeFilter(request.FilterSettings) if err != nil { return usersync.Request{}, privacy.Policies{}, err } gdprRequestInfo := gdpr.RequestInfo{ - Consent: request.GDPRConsent, + Consent: privacyPolicies.GDPR.Consent, GDPRSignal: gdprSignal, } @@ -195,12 +179,89 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr Privacy: usersyncPrivacy{ gdprPermissions: gdprPerms, ccpaParsedPolicy: ccpaParsedPolicy, + activityControl: activityControl, }, SyncTypeFilter: syncTypeFilter, } return rx, privacyPolicies, nil } +func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (privacy.Policies, gdpr.Signal, error) { + // GDPR + gppSID, err := stringutil.StrToInt8Slice(request.GPPSid) + if err != nil { + return privacy.Policies{}, gdpr.SignalNo, err + } + + gdprSignal, gdprString, err := extractGDPRSignal(request.GDPR, gppSID) + if err != nil { + return privacy.Policies{}, gdpr.SignalNo, err + } + + var gpp gpplib.GppContainer + if len(request.GPP) > 0 { + var err error + gpp, err = gpplib.Parse(request.GPP) + if err != nil { + return privacy.Policies{}, gdpr.SignalNo, err + } + } + + gdprConsent := request.GDPRConsent + if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 { + gdprConsent = gpp.Sections[i].GetValue() + } + + if gdprConsent == "" { + if gdprSignal == gdpr.SignalYes { + return privacy.Policies{}, gdpr.SignalNo, errCookieSyncGDPRConsentMissing + } + + if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, usersyncDefaultGDPRValue) == gdpr.SignalYes { + return privacy.Policies{}, gdpr.SignalNo, errCookieSyncGDPRConsentMissingSignalAmbiguous + } + } + + // CCPA + ccpaString, err := ccpa.SelectCCPAConsent(request.USPrivacy, gpp, gppSID) + if err != nil { + return privacy.Policies{}, gdpr.SignalNo, err + } + + return privacy.Policies{ + GDPR: gdprPrivacy.Policy{ + Signal: gdprString, + Consent: gdprConsent, + }, + CCPA: ccpa.Policy{ + Consent: ccpaString, + }, + GPP: gppPrivacy.Policy{ + Consent: request.GPP, + RawSID: request.GPPSid, + }, + }, gdprSignal, nil +} + +func extractGDPRSignal(requestGDPR *int, gppSID []int8) (gdpr.Signal, string, error) { + if len(gppSID) > 0 { + if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) { + return gdpr.SignalYes, strconv.Itoa(int(gdpr.SignalYes)), nil + } + return gdpr.SignalNo, strconv.Itoa(int(gdpr.SignalNo)), nil + } + + if requestGDPR == nil { + return gdpr.SignalAmbiguous, "", nil + } + + gdprSignal, err := gdpr.IntSignalParse(*requestGDPR) + if err != nil { + return gdpr.SignalAmbiguous, strconv.Itoa(*requestGDPR), err + } + return gdprSignal, strconv.Itoa(*requestGDPR), nil +} + func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) { switch err { case errCookieSyncAccountBlocked: @@ -448,6 +509,7 @@ type usersyncPrivacyConfig struct { type usersyncPrivacy struct { gdprPermissions gdpr.Permissions ccpaParsedPolicy ccpa.ParsedPolicy + activityControl privacy.ActivityControl } func (p usersyncPrivacy) GDPRAllowsHostCookie() bool { @@ -464,3 +526,8 @@ func (p usersyncPrivacy) CCPAAllowsBidderSync(bidder string) bool { enforce := p.ccpaParsedPolicy.CanEnforce() && p.ccpaParsedPolicy.ShouldEnforce(bidder) return !enforce } + +func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool { + return p.activityControl.Allow(privacy.ActivitySyncUser, + privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidder}) +} diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 3947d1f06ba..45c27f0ea31 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -7,10 +7,10 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "strings" "testing" "testing/iotest" - "time" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" @@ -23,6 +23,7 @@ import ( gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr" gppPrivacy "github.com/prebid/prebid-server/privacy/gpp" "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -104,7 +105,7 @@ func TestCookieSyncHandle(t *testing.T) { syncer.On("GetSync", syncTypeExpected, privacy.Policies{}).Return(sync, nil).Maybe() cookieWithSyncs := usersync.NewCookie() - cookieWithSyncs.TrySync("foo", "anyID") + cookieWithSyncs.Sync("foo", "anyID") testCases := []struct { description string @@ -269,7 +270,9 @@ func TestCookieSyncHandle(t *testing.T) { request := httptest.NewRequest("POST", "/cookiesync", test.givenBody) if test.givenCookie != nil { - request.AddCookie(test.givenCookie.ToHTTPCookie(24 * time.Hour)) + httpCookie, err := ToHTTPCookie(test.givenCookie) + assert.NoError(t, err) + request.AddCookie(httpCookie) } writer := httptest.NewRecorder() @@ -303,6 +306,185 @@ func TestCookieSyncHandle(t *testing.T) { } } +func TestExtractGDPRSignal(t *testing.T) { + type testInput struct { + requestGDPR *int + gppSID []int8 + } + type testOutput struct { + gdprSignal gdpr.Signal + gdprString string + err error + } + testCases := []struct { + desc string + in testInput + expected testOutput + }{ + { + desc: "SectionTCFEU2 is listed in GPP_SID array, expect SignalYes and nil error", + in: testInput{ + requestGDPR: nil, + gppSID: []int8{2}, + }, + expected: testOutput{ + gdprSignal: gdpr.SignalYes, + gdprString: strconv.Itoa(int(gdpr.SignalYes)), + err: nil, + }, + }, + { + desc: "SectionTCFEU2 is not listed in GPP_SID array, expect SignalNo and nil error", + in: testInput{ + requestGDPR: nil, + gppSID: []int8{6}, + }, + expected: testOutput{ + gdprSignal: gdpr.SignalNo, + gdprString: strconv.Itoa(int(gdpr.SignalNo)), + err: nil, + }, + }, + { + desc: "Empty GPP_SID array and nil requestGDPR value, expect SignalAmbiguous and nil error", + in: testInput{ + requestGDPR: nil, + gppSID: []int8{}, + }, + expected: testOutput{ + gdprSignal: gdpr.SignalAmbiguous, + gdprString: "", + err: nil, + }, + }, + { + desc: "Empty GPP_SID array and non-nil requestGDPR value that could not be successfully parsed, expect SignalAmbiguous and parse error", + in: testInput{ + requestGDPR: ptrutil.ToPtr(2), + gppSID: nil, + }, + expected: testOutput{ + gdprSignal: gdpr.SignalAmbiguous, + gdprString: "2", + err: &errortypes.BadInput{"GDPR signal should be integer 0 or 1"}, + }, + }, + { + desc: "Empty GPP_SID array and non-nil requestGDPR value that could be successfully parsed, expect SignalYes and nil error", + in: testInput{ + requestGDPR: ptrutil.ToPtr(1), + gppSID: nil, + }, + expected: testOutput{ + gdprSignal: gdpr.SignalYes, + gdprString: "1", + err: nil, + }, + }, + } + for _, tc := range testCases { + // run + outSignal, outGdprStr, outErr := extractGDPRSignal(tc.in.requestGDPR, tc.in.gppSID) + // assertions + assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc) + assert.Equal(t, tc.expected.gdprString, outGdprStr, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + } +} + +func TestExtractPrivacyPolicies(t *testing.T) { + type testInput struct { + request cookieSyncRequest + usersyncDefaultGDPRValue string + } + type testOutput struct { + policies privacy.Policies + gdprSignal gdpr.Signal + err error + } + testCases := []struct { + desc string + in testInput + expected testOutput + }{ + { + desc: "request GPP string is malformed, expect empty policies, signal No and error", + in: testInput{ + request: cookieSyncRequest{GPP: "malformedGPPString"}, + }, + expected: testOutput{ + policies: privacy.Policies{}, + gdprSignal: gdpr.SignalNo, + err: errors.New("error parsing GPP header, header must have type=3"), + }, + }, + { + desc: "Malformed GPPSid string", + in: testInput{ + request: cookieSyncRequest{ + GPP: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN", + GPPSid: "malformed", + USPrivacy: "1YYY", + }, + }, + expected: testOutput{ + policies: privacy.Policies{}, + gdprSignal: gdpr.SignalNo, + err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax}, + }, + }, + { + desc: "request USPrivacy string is different from the one in the GPP string, expect empty policies, signalNo and error", + in: testInput{ + request: cookieSyncRequest{ + GPP: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN", + GPPSid: "6", + USPrivacy: "1YYY", + }, + }, + expected: testOutput{ + policies: privacy.Policies{}, + gdprSignal: gdpr.SignalNo, + err: errors.New("request.us_privacy consent does not match uspv1"), + }, + }, + { + desc: "no issues extracting privacy policies from request GPP and request GPPSid strings", + in: testInput{ + request: cookieSyncRequest{ + GPP: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN", + GPPSid: "6", + }, + }, + expected: testOutput{ + policies: privacy.Policies{ + GDPR: gdprPrivacy.Policy{ + Signal: "0", + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + }, + CCPA: ccpa.Policy{ + Consent: "1YNN", + }, + GPP: gppPrivacy.Policy{ + Consent: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN", + RawSID: "6", + }, + }, + gdprSignal: gdpr.SignalNo, + err: nil, + }, + }, + } + for _, tc := range testCases { + // run + outPolicies, outSignal, outErr := extractPrivacyPolicies(tc.in.request, tc.in.usersyncDefaultGDPRValue) + // assertions + assert.Equal(t, tc.expected.policies, outPolicies, tc.desc) + assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + } +} + func TestCookieSyncParseRequest(t *testing.T) { expectedCCPAParsedPolicy, _ := ccpa.Policy{Consent: "1NYN"}.Parse(map[string]struct{}{}) @@ -317,14 +499,15 @@ func TestCookieSyncParseRequest(t *testing.T) { expectedPrivacy privacy.Policies expectedRequest usersync.Request }{ + { - description: "Complete Request", + description: "Complete Request - includes GPP string with EU TCF V2", givenBody: strings.NewReader(`{` + `"bidders":["a", "b"],` + `"gdpr":1,` + `"gdpr_consent":"anyGDPRConsent",` + `"us_privacy":"1NYN",` + - `"gpp":"anyGPPString",` + + `"gpp":"DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",` + `"gpp_sid":"2",` + `"limit":42,` + `"coopSync":true,` + @@ -341,13 +524,13 @@ func TestCookieSyncParseRequest(t *testing.T) { expectedPrivacy: privacy.Policies{ GDPR: gdprPrivacy.Policy{ Signal: "1", - Consent: "anyGDPRConsent", + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", }, CCPA: ccpa.Policy{ Consent: "1NYN", }, GPP: gppPrivacy.Policy{ - Consent: "anyGPPString", + Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", RawSID: "2", }, }, @@ -791,6 +974,38 @@ func TestCookieSyncParseRequest(t *testing.T) { expectedError: errCookieSyncAccountBlocked.Error(), givenAccountRequired: true, }, + + { + description: "Account Defaults - Invalid Activities", + givenBody: strings.NewReader(`{` + + `"bidders":["a", "b"],` + + `"account":"ValidAccountInvalidActivities"` + + `}`), + givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"}, + givenCCPAEnabled: true, + givenConfig: config.UserSync{ + Cooperative: config.UserSyncCooperative{ + EnabledByDefault: false, + PriorityGroups: [][]string{{"a", "b", "c"}}, + }, + }, + expectedPrivacy: privacy.Policies{}, + expectedRequest: usersync.Request{ + Bidders: []string{"a", "b"}, + Cooperative: usersync.Cooperative{ + Enabled: false, + PriorityGroups: [][]string{{"a", "b", "c"}}, + }, + Limit: 0, + Privacy: usersyncPrivacy{ + gdprPermissions: &fakePermissions{}, + }, + SyncTypeFilter: usersync.SyncTypeFilter{ + IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude), + Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude), + }, + }, + }, } for _, test := range testCases { @@ -815,8 +1030,9 @@ func TestCookieSyncParseRequest(t *testing.T) { ccpaEnforce: test.givenCCPAEnabled, }, accountsFetcher: FakeAccountsFetcher{AccountData: map[string]json.RawMessage{ - "TestAccount": json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`), - "DisabledAccount": json.RawMessage(`{"disabled":true}`), + "TestAccount": json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`), + "DisabledAccount": json.RawMessage(`{"disabled":true}`), + "ValidAccountInvalidActivities": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`), }}, } assert.NoError(t, endpoint.config.MarshalAccountDefaults()) @@ -1482,7 +1698,7 @@ func TestCookieSyncHandleResponse(t *testing.T) { cookie := usersync.NewCookie() if test.givenCookieHasSyncs { - if err := cookie.TrySync("foo", "anyID"); err != nil { + if err := cookie.Sync("foo", "anyID"); err != nil { assert.FailNow(t, test.description+":set_cookie") } } @@ -1689,6 +1905,46 @@ func TestUsersyncPrivacyCCPAAllowsBidderSync(t *testing.T) { } } +func TestCookieSyncActivityControlIntegration(t *testing.T) { + testCases := []struct { + name string + bidderName string + accountPrivacy *config.AccountPrivacy + expectedResult bool + }{ + { + name: "activity_is_allowed", + bidderName: "bidderA", + accountPrivacy: getDefaultActivityConfig("bidderA", true), + expectedResult: true, + }, + { + name: "activity_is_denied", + bidderName: "bidderA", + accountPrivacy: getDefaultActivityConfig("bidderA", false), + expectedResult: false, + }, + { + name: "activity_is_abstain", + bidderName: "bidderA", + accountPrivacy: nil, + expectedResult: true, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + activities, err := privacy.NewActivityControl(test.accountPrivacy) + assert.NoError(t, err) + up := usersyncPrivacy{ + activityControl: activities, + } + actualResult := up.ActivityAllowsUserSync(test.bidderName) + assert.Equal(t, test.expectedResult, actualResult) + }) + } +} + func TestCombineErrors(t *testing.T) { testCases := []struct { description string @@ -1849,3 +2105,22 @@ func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCo AllowBidRequest: true, }, nil } + +func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy { + return &config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + SyncUser: config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: allow, + Condition: config.ActivityCondition{ + ComponentName: []string{componentName}, + ComponentType: []string{"bidder"}, + }, + }, + }, + }, + }, + } +} diff --git a/endpoints/events/event.go b/endpoints/events/event.go index ca323a612e3..089d5606552 100644 --- a/endpoints/events/event.go +++ b/endpoints/events/event.go @@ -107,8 +107,8 @@ func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprou return } - // account does not support events - if !account.EventsEnabled { + // Check if events are enabled for the account + if !account.Events.IsEnabled() { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(fmt.Sprintf("Account '%s' doesn't support events", eventRequest.AccountID))) return @@ -217,7 +217,7 @@ func HandleAccountServiceErrors(errs []error) (status int, messages []string) { } } - return status, messages + return } func optionalParameters(request *analytics.EventRequest) string { diff --git a/endpoints/events/event_test.go b/endpoints/events/event_test.go index d4bbc991344..95711ba1328 100644 --- a/endpoints/events/event_test.go +++ b/endpoints/events/event_test.go @@ -73,9 +73,9 @@ func (e *eventsMockAnalyticsModule) LogNotificationEventObject(ne *analytics.Not // Mock Account fetcher var mockAccountData = map[string]json.RawMessage{ - "events_enabled": json.RawMessage(`{"events_enabled":true}`), - "events_disabled": json.RawMessage(`{"events_enabled":false}`), - "malformed_acct": json.RawMessage(`{"events_enabled":"invalid type"}`), + "events_enabled": json.RawMessage(`{"events": {"enabled":true}}`), + "events_disabled": json.RawMessage(`{"events": {"enabled":false}}`), + "malformed_acct": json.RawMessage(`{"events": {"enabled":"invalid type"}}`), } type mockAccountsFetcher struct { @@ -359,7 +359,7 @@ func TestShouldReturnBadRequestWhenAnalyticsValueIsInvalid(t *testing.T) { assert.Equal(t, "invalid request: unknown analytics: '4'\n", string(d)) } -func TestShouldNotPassEventToAnalyticsReporterWhenAccountNotFound(t *testing.T) { +func TestShouldNotPassEventToAnalyticsReporterWhenAccountNotFoundAndDefaultIsFalse(t *testing.T) { // mock AccountsFetcher mockAccountsFetcher := &mockAccountsFetcher{ diff --git a/endpoints/getuids.go b/endpoints/getuids.go index ad984a8df00..f420c64fa6b 100644 --- a/endpoints/getuids.go +++ b/endpoints/getuids.go @@ -18,9 +18,11 @@ type userSyncs struct { // returns all the existing syncs for the user func NewGetUIDsEndpoint(cfg config.HostCookie) httprouter.Handle { return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pc := usersync.ParseCookieFromRequest(r, &cfg) + cookie := usersync.ReadCookie(r, usersync.Base64Decoder{}, &cfg) + usersync.SyncHostCookie(r, cookie, &cfg) + userSyncs := new(userSyncs) - userSyncs.BuyerUIDs = pc.GetUIDs() + userSyncs.BuyerUIDs = cookie.GetUIDs() json.NewEncoder(w).Encode(userSyncs) }) } diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 8634710ef87..142f7320e27 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/prebid/prebid-server/privacy" "net/http" "net/url" "strings" @@ -27,6 +28,7 @@ import ( "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/exchange" + "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/hooks" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" @@ -67,6 +69,7 @@ func NewAmpEndpoint( bidderMap map[string]openrtb_ext.BidderName, storedRespFetcher stored_requests.Fetcher, hookExecutionPlanBuilder hooks.ExecutionPlanBuilder, + tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, ) (httprouter.Handle, error) { if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { @@ -98,7 +101,9 @@ func NewAmpEndpoint( nil, ipValidator, storedRespFetcher, - hookExecutionPlanBuilder}).AmpAuction), nil + hookExecutionPlanBuilder, + tmaxAdjustments, + }).AmpAuction), nil } @@ -169,7 +174,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } - ao.Request = reqWrapper.BidRequest + ao.RequestWrapper = reqWrapper ctx := context.Background() var cancel context.CancelFunc @@ -180,12 +185,15 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h } defer cancel() - usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie)) + // Read UserSyncs/Cookie from Request + usersyncs := usersync.ReadCookie(r, usersync.Base64Decoder{}, &deps.cfg.HostCookie) + usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie) if usersyncs.HasAnyLiveSyncs() { labels.CookieFlag = metrics.CookieFlagYes } else { labels.CookieFlag = metrics.CookieFlagNo } + labels.PubID = getAccountID(reqWrapper.Site.Publisher) // Look up account now that we have resolved the pubID value account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine) @@ -219,9 +227,18 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } + tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR) + + activities, activitiesErr := privacy.NewActivityControl(&account.Privacy) + if activitiesErr != nil { + errL = append(errL, activitiesErr) + writeError(errL, w, &labels) + return + } + secGPC := r.Header.Get("Sec-GPC") - auctionRequest := exchange.AuctionRequest{ + auctionRequest := &exchange.AuctionRequest{ BidRequestWrapper: reqWrapper, Account: *account, UserSyncs: usersyncs, @@ -235,9 +252,22 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h PubID: labels.PubID, HookExecutor: hookExecutor, QueryParams: r.URL.Query(), + TCF2Config: tcf2Config, + Activities: activities, + TmaxAdjustments: deps.tmaxAdjustments, } - response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) + auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) + defer func() { + if !auctionRequest.BidderResponseStartTime.IsZero() { + deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime)) + } + }() + var response *openrtb2.BidResponse + if auctionResponse != nil { + response = auctionResponse.BidResponse + } + ao.SeatNonBid = auctionResponse.GetSeatNonBid() ao.AuctionResponse = response rejectErr, isRejectErr := hookexecution.CastRejectErr(err) if err != nil && !isRejectErr { @@ -265,7 +295,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } - labels, ao = sendAmpResponse(w, hookExecutor, response, reqWrapper, account, labels, ao, errL) + labels, ao = sendAmpResponse(w, hookExecutor, auctionResponse, reqWrapper, account, labels, ao, errL) } func rejectAmpRequest( @@ -282,19 +312,23 @@ func rejectAmpRequest( ao.AuctionResponse = response ao.Errors = append(ao.Errors, rejectErr) - return sendAmpResponse(w, hookExecutor, response, reqWrapper, account, labels, ao, errs) + return sendAmpResponse(w, hookExecutor, &exchange.AuctionResponse{BidResponse: response}, reqWrapper, account, labels, ao, errs) } func sendAmpResponse( w http.ResponseWriter, hookExecutor hookexecution.HookStageExecutor, - response *openrtb2.BidResponse, + auctionResponse *exchange.AuctionResponse, reqWrapper *openrtb_ext.RequestWrapper, account *config.Account, labels metrics.Labels, ao analytics.AmpObject, errs []error, ) (metrics.Labels, analytics.AmpObject) { + var response *openrtb2.BidResponse + if auctionResponse != nil { + response = auctionResponse.BidResponse + } hookExecutor.ExecuteAuctionResponseStage(response) // Need to extract the targeting parameters from the response, as those are all that // go in the AMP response @@ -345,7 +379,7 @@ func sendAmpResponse( } // Now JSONify the targets for the AMP response. ampResponse := AmpResponse{Targeting: targets} - ao, ampResponse.ORTB2.Ext = getExtBidResponse(hookExecutor, response, reqWrapper, account, ao, errs) + ao, ampResponse.ORTB2.Ext = getExtBidResponse(hookExecutor, auctionResponse, reqWrapper, account, ao, errs) ao.AmpTargetingValues = targets @@ -366,12 +400,16 @@ func sendAmpResponse( func getExtBidResponse( hookExecutor hookexecution.HookStageExecutor, - response *openrtb2.BidResponse, + auctionResponse *exchange.AuctionResponse, reqWrapper *openrtb_ext.RequestWrapper, account *config.Account, ao analytics.AmpObject, errs []error, ) (analytics.AmpObject, openrtb_ext.ExtBidResponse) { + var response *openrtb2.BidResponse + if auctionResponse != nil { + response = auctionResponse.BidResponse + } // Extract any errors var extResponse openrtb_ext.ExtBidResponse eRErr := json.Unmarshal(response.Ext, &extResponse) @@ -423,6 +461,8 @@ func getExtBidResponse( } } + setSeatNonBid(&extBidResponse, reqWrapper, auctionResponse) + return ao, extBidResponse } @@ -782,7 +822,7 @@ func setEffectiveAmpPubID(req *openrtb2.BidRequest, account string) { } } -func setTrace(req *openrtb2.BidRequest, value string) (err error) { +func setTrace(req *openrtb2.BidRequest, value string) error { if value == "" { return nil } @@ -802,3 +842,23 @@ func setTrace(req *openrtb2.BidRequest, value string) (err error) { return nil } + +// setSeatNonBid populates bidresponse.ext.prebid.seatnonbid if bidrequest.ext.prebid.returnallbidstatus is true +func setSeatNonBid(finalExtBidResponse *openrtb_ext.ExtBidResponse, request *openrtb_ext.RequestWrapper, auctionResponse *exchange.AuctionResponse) bool { + if finalExtBidResponse == nil || auctionResponse == nil || request == nil { + return false + } + reqExt, err := request.GetRequestExt() + if err != nil { + return false + } + prebid := reqExt.GetPrebid() + if prebid == nil || !prebid.ReturnAllBidStatus { + return false + } + if finalExtBidResponse.Prebid == nil { + finalExtBidResponse.Prebid = &openrtb_ext.ExtResponsePrebid{} + } + finalExtBidResponse.Prebid.SeatNonBid = auctionResponse.GetSeatNonBid() + return true +} diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 2d4341d05ad..87033f71b64 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -88,7 +88,7 @@ func TestGoodAmpRequests(t *testing.T) { continue } - test.storedRequest = map[string]json.RawMessage{tagID: test.BidRequest} + test.StoredRequest = map[string]json.RawMessage{tagID: test.BidRequest} test.endpointType = AMP_ENDPOINT cfg := &config.Configuration{ @@ -165,7 +165,7 @@ func TestAccountErrors(t *testing.T) { if !assert.NoError(t, json.Unmarshal(fileJsonData, &test), "Failed to unmarshal data from file: %s. Error: %v", tt.filename, err) { continue } - test.storedRequest = map[string]json.RawMessage{tt.storedReqID: test.BidRequest} + test.StoredRequest = map[string]json.RawMessage{tt.storedReqID: test.BidRequest} test.endpointType = AMP_ENDPOINT cfg := &config.Configuration{ @@ -216,6 +216,7 @@ func TestAMPPageInfo(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&curl=%s", url.QueryEscape(page)), nil) recorder := httptest.NewRecorder() @@ -319,6 +320,7 @@ func TestGDPRConsent(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) // Invoke Endpoint @@ -742,6 +744,7 @@ func TestCCPAConsent(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) // Invoke Endpoint @@ -855,6 +858,7 @@ func TestConsentWarnings(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) // Invoke Endpoint @@ -953,6 +957,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) // Invoke Endpoint @@ -1007,6 +1012,7 @@ func TestAMPSiteExt(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) request, err := http.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) if !assert.NoError(t, err) { @@ -1049,6 +1055,7 @@ func TestAmpBadRequests(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) for requestID := range badRequests { request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil) @@ -1082,6 +1089,7 @@ func TestAmpDebug(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) for requestID := range requests { @@ -1217,6 +1225,7 @@ func TestQueryParamOverrides(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) requestID := "1" @@ -1374,6 +1383,7 @@ func (s formatOverrideSpec) execute(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s&account=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize, s.account) @@ -1420,7 +1430,7 @@ var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBi }, } -func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { r := auctionRequest.BidRequestWrapper m.lastRequest = r.BidRequest @@ -1454,12 +1464,12 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest exchan response.Ext = json.RawMessage(fmt.Sprintf(`{"debug": {"httpcalls": {}, "resolvedrequest": %s}}`, resolvedRequest)) } - return response, nil + return &exchange.AuctionResponse{BidResponse: response}, nil } type mockAmpExchangeWarnings struct{} -func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { response := &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{{ Bid: []openrtb2.Bid{{ @@ -1469,7 +1479,7 @@ func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r exchange.Au }}, Ext: json.RawMessage(`{ "warnings": {"appnexus": [{"code": 10003, "message": "debug turned off for bidder"}] }}`), } - return response, nil + return &exchange.AuctionResponse{BidResponse: response}, nil } func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, regsExt *openrtb_ext.ExtRegs) ([]byte, error) { @@ -1694,33 +1704,35 @@ func TestBuildAmpObject(t *testing.T) { expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: nil, - Request: &openrtb2.BidRequest{ - ID: "some-request-id", - Device: &openrtb2.Device{ - IP: "192.0.2.1", - }, - Site: &openrtb2.Site{ - Page: "prebid.org", - Ext: json.RawMessage(`{"amp":1}`), - }, - Imp: []openrtb2.Imp{ - { - ID: "some-impression-id", - Banner: &openrtb2.Banner{ - Format: []openrtb2.Format{ - { - W: 300, - H: 250, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Device: &openrtb2.Device{ + IP: "192.0.2.1", + }, + Site: &openrtb2.Site{ + Page: "prebid.org", + Ext: json.RawMessage(`{"amp":1}`), + }, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 300, + H: 250, + }, }, }, + Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), }, - Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), - Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), }, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`), }, - AT: 1, - TMax: 500, - Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true}}}`), }, AuctionResponse: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{{ @@ -1748,33 +1760,35 @@ func TestBuildAmpObject(t *testing.T) { expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: nil, - Request: &openrtb2.BidRequest{ - ID: "some-request-id", - Device: &openrtb2.Device{ - IP: "192.0.2.1", - }, - Site: &openrtb2.Site{ - Page: "prebid.org", - Ext: json.RawMessage(`{"amp":1}`), - }, - Imp: []openrtb2.Imp{ - { - ID: "some-impression-id", - Banner: &openrtb2.Banner{ - Format: []openrtb2.Format{ - { - W: 300, - H: 250, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Device: &openrtb2.Device{ + IP: "192.0.2.1", + }, + Site: &openrtb2.Site{ + Page: "prebid.org", + Ext: json.RawMessage(`{"amp":1}`), + }, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 300, + H: 250, + }, }, }, + Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), }, - Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), - Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), }, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`), }, - AT: 1, - TMax: 500, - Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true}}}`), }, AuctionResponse: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{{ @@ -1813,7 +1827,15 @@ func TestBuildAmpObject(t *testing.T) { // assert AmpObject assert.Equalf(t, test.expectedAmpObject.Status, actualAmpObject.Status, "Amp Object Status field doesn't match expected: %s\n", test.description) assert.Lenf(t, actualAmpObject.Errors, len(test.expectedAmpObject.Errors), "Amp Object Errors array doesn't match expected: %s\n", test.description) - assert.Equalf(t, test.expectedAmpObject.Request, actualAmpObject.Request, "Amp Object BidRequest doesn't match expected: %s\n", test.description) + var expectedRequest *openrtb2.BidRequest + var actualRequest *openrtb2.BidRequest + if test.expectedAmpObject.RequestWrapper != nil { + expectedRequest = test.expectedAmpObject.RequestWrapper.BidRequest + } + if actualAmpObject.RequestWrapper != nil { + actualRequest = test.expectedAmpObject.RequestWrapper.BidRequest + } + assert.Equalf(t, expectedRequest, actualRequest, "Amp Object BidRequest doesn't match expected: %s\n", test.description) assert.Equalf(t, test.expectedAmpObject.AuctionResponse, actualAmpObject.AuctionResponse, "Amp Object BidResponse doesn't match expected: %s\n", test.description) assert.Equalf(t, test.expectedAmpObject.AmpTargetingValues, actualAmpObject.AmpTargetingValues, "Amp Object AmpTargetingValues doesn't match expected: %s\n", test.description) assert.Equalf(t, test.expectedAmpObject.Origin, actualAmpObject.Origin, "Amp Object Origin field doesn't match expected: %s\n", test.description) @@ -1874,7 +1896,7 @@ func TestIdGeneration(t *testing.T) { // Set up and run test actualAmpObject, endpoint := ampObjectTestSetup(t, "test", test.givenInStoredRequest, test.givenGenerateRequestID, &mockAmpExchange{}) endpoint(recorder, request, nil) - assert.Equalf(t, test.expectedID, actualAmpObject.Request.ID, "Bid Request ID is incorrect: %s\n", test.description) + assert.Equalf(t, test.expectedID, actualAmpObject.RequestWrapper.ID, "Bid Request ID is incorrect: %s\n", test.description) } } @@ -1902,6 +1924,7 @@ func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMe openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) return &actualAmpObject, endpoint } @@ -1954,6 +1977,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) for _, test := range testCases { @@ -1989,6 +2013,7 @@ func TestRequestWithTargeting(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) url, err := url.Parse("/openrtb2/auction/amp") assert.NoError(t, err, "unexpected error received while parsing url") @@ -2108,30 +2133,30 @@ func TestValidAmpResponseWhenRequestRejected(t *testing.T) { { description: "Assert correct AmpResponse when request rejected at entrypoint stage", file: "sample-requests/hooks/amp_entrypoint_reject.json", - planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr, nil})}, }, { // raw_auction stage not executed for AMP endpoint, so we expect full response description: "Assert correct AmpResponse when request rejected at raw_auction stage", file: "sample-requests/amp/valid-supplementary/aliased-buyeruids.json", - planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr, nil})}, }, { description: "Assert correct AmpResponse when request rejected at processed_auction stage", file: "sample-requests/hooks/amp_processed_auction_request_reject.json", - planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr, nil})}, }, { // bidder_request stage rejects only bidder, so we expect bidder rejection warning added description: "Assert correct AmpResponse when request rejected at bidder-request stage", file: "sample-requests/hooks/amp_bidder_reject.json", - planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr, nil})}, }, { // raw_bidder_response stage rejects only bidder, so we expect bidder rejection warning added description: "Assert correct AmpResponse when request rejected at raw_bidder_response stage", file: "sample-requests/hooks/amp_bidder_response_reject.json", - planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr, nil})}, }, { // no debug information should be added for raw_auction stage because it's not executed for amp endpoint @@ -2178,7 +2203,7 @@ func TestValidAmpResponseWhenRequestRejected(t *testing.T) { query := request.URL.Query() tagID := query.Get("tag_id") - test.storedRequest = map[string]json.RawMessage{tagID: test.BidRequest} + test.StoredRequest = map[string]json.RawMessage{tagID: test.BidRequest} test.planBuilder = tc.planBuilder test.endpointType = AMP_ENDPOINT @@ -2303,7 +2328,7 @@ func TestSendAmpResponse_LogsErrors(t *testing.T) { account := &config.Account{DebugAllow: true} reqWrapper := openrtb_ext.RequestWrapper{BidRequest: test.request} - labels, ao = sendAmpResponse(test.writer, test.hookExecutor, test.response, &reqWrapper, account, labels, ao, nil) + labels, ao = sendAmpResponse(test.writer, test.hookExecutor, &exchange.AuctionResponse{BidResponse: test.response}, &reqWrapper, account, labels, ao, nil) assert.Equal(t, ao.Errors, test.expectedErrors, "Invalid errors.") assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.") @@ -2322,3 +2347,64 @@ func (e errorResponseWriter) Write(bytes []byte) (int, error) { } func (e errorResponseWriter) WriteHeader(statusCode int) {} + +func TestSetSeatNonBid(t *testing.T) { + type args struct { + finalExtBidResponse *openrtb_ext.ExtBidResponse + request *openrtb_ext.RequestWrapper + auctionResponse *exchange.AuctionResponse + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "nil-auctionResponse", + args: args{auctionResponse: nil}, + want: false, + }, + { + name: "nil-request", + args: args{auctionResponse: &exchange.AuctionResponse{}, request: nil}, + want: false, + }, + { + name: "invalid-req-ext", + args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`invalid json`)}}}, + want: false, + }, + { + name: "nil-prebid", + args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: nil}}}, + want: false, + }, + { + name: "returnallbidstatus-is-false", + args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : false}}`)}}}, + want: false, + }, + { + name: "finalExtBidResponse-is-nil", + args: args{finalExtBidResponse: nil}, + want: false, + }, + { + name: "returnallbidstatus-is-true-and-responseExt.Prebid-is-nil", + args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : true}}`)}}}, + want: true, + }, + { + name: "returnallbidstatus-is-true-and-responseExt.Prebid-is-not-nil", + args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : true}}`)}}}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := setSeatNonBid(tt.args.finalExtBidResponse, tt.args.request, tt.args.auctionResponse); got != tt.want { + t.Errorf("setSeatNonBid() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 813cbe81195..6fe5aab9535 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -1,6 +1,7 @@ package openrtb2 import ( + "compress/gzip" "context" "encoding/json" "errors" @@ -13,6 +14,8 @@ import ( "strings" "time" + "github.com/prebid/prebid-server/privacy" + "github.com/buger/jsonparser" "github.com/gofrs/uuid" "github.com/golang/glog" @@ -24,6 +27,7 @@ import ( nativeRequests "github.com/prebid/openrtb/v19/native1/request" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/openrtb/v19/openrtb3" + "github.com/prebid/prebid-server/bidadjustment" "github.com/prebid/prebid-server/hooks" "github.com/prebid/prebid-server/ortb" "golang.org/x/net/publicsuffix" @@ -35,6 +39,7 @@ import ( "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/exchange" + "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/hooks/hookexecution" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" @@ -87,6 +92,7 @@ func NewEndpoint( bidderMap map[string]openrtb_ext.BidderName, storedRespFetcher stored_requests.Fetcher, hookExecutionPlanBuilder hooks.ExecutionPlanBuilder, + tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, ) (httprouter.Handle, error) { if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { return nil, errors.New("NewEndpoint requires non-nil arguments.") @@ -117,7 +123,8 @@ func NewEndpoint( nil, ipValidator, storedRespFetcher, - hookExecutionPlanBuilder}).Auction), nil + hookExecutionPlanBuilder, + tmaxAdjustments}).Auction), nil } type endpointDeps struct { @@ -139,6 +146,7 @@ type endpointDeps struct { privateNetworkIPValidator iputil.IPValidator storedRespFetcher stored_requests.Fetcher hookExecutionPlanBuilder hooks.ExecutionPlanBuilder + tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -179,11 +187,22 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http } if rejectErr := hookexecution.FindFirstRejectOrNil(errL); rejectErr != nil { - ao.Request = req.BidRequest + ao.RequestWrapper = req labels, ao = rejectAuctionRequest(*rejectErr, w, hookExecutor, req.BidRequest, account, labels, ao) return } + tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR) + + activities, activitiesErr := privacy.NewActivityControl(&account.Privacy) + if activitiesErr != nil { + errL = append(errL, activitiesErr) + if errortypes.ContainsFatalError(errL) { + writeError(errL, w, &labels) + return + } + } + ctx := context.Background() timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(req.TMax) * time.Millisecond) @@ -193,7 +212,11 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http defer cancel() } - usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie)) + // Read Usersyncs/Cookie + decoder := usersync.Base64Decoder{} + usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie) + usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie) + if req.Site != nil { if usersyncs.HasAnyLiveSyncs() { labels.CookieFlag = metrics.CookieFlagYes @@ -213,7 +236,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http warnings := errortypes.WarningOnly(errL) - auctionRequest := exchange.AuctionRequest{ + auctionRequest := &exchange.AuctionRequest{ BidRequestWrapper: req, Account: *account, UserSyncs: usersyncs, @@ -228,11 +251,24 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http BidderImpReplaceImpID: bidderImpReplaceImp, PubID: labels.PubID, HookExecutor: hookExecutor, + TCF2Config: tcf2Config, + Activities: activities, + TmaxAdjustments: deps.tmaxAdjustments, } - response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) - ao.Request = req.BidRequest - ao.Response = response + auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) + defer func() { + if !auctionRequest.BidderResponseStartTime.IsZero() { + deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime)) + } + }() + ao.RequestWrapper = req ao.Account = account + var response *openrtb2.BidResponse + if auctionResponse != nil { + response = auctionResponse.BidResponse + } + ao.Response = response + ao.SeatNonBid = auctionResponse.GetSeatNonBid() rejectErr, isRejectErr := hookexecution.CastRejectErr(err) if err != nil && !isRejectErr { if errortypes.ReadCode(err) == errortypes.BadInputErrorCode { @@ -251,9 +287,40 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http return } + err = setSeatNonBidRaw(req, auctionResponse) + if err != nil { + glog.Errorf("Error setting seat non-bid: %v", err) + } labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao) } +// setSeatNonBidRaw is transitional function for setting SeatNonBid inside bidResponse.Ext +// Because, +// 1. today exchange.HoldAuction prepares and marshals some piece of response.Ext which is then used by auction.go, amp_auction.go and video_auction.go +// 2. As per discussion with Prebid Team we are planning to move away from - HoldAuction building openrtb2.BidResponse. instead respective auction modules will build this object +// 3. So, we will need this method to do first, unmarshalling of response.Ext +func setSeatNonBidRaw(request *openrtb_ext.RequestWrapper, auctionResponse *exchange.AuctionResponse) error { + if auctionResponse == nil || auctionResponse.BidResponse == nil { + return nil + } + // unmarshalling is required here, until we are moving away from bidResponse.Ext, which is populated + // by HoldAuction + response := auctionResponse.BidResponse + respExt := &openrtb_ext.ExtBidResponse{} + if err := json.Unmarshal(response.Ext, &respExt); err != nil { + return err + } + if setSeatNonBid(respExt, request, auctionResponse) { + if respExtJson, err := json.Marshal(respExt); err == nil { + response.Ext = respExtJson + return nil + } else { + return err + } + } + return nil +} + func rejectAuctionRequest( rejectErr hookexecution.RejectError, w http.ResponseWriter, @@ -331,28 +398,49 @@ func sendAuctionResponse( // // If the errors list has at least one element, then no guarantees are made about the returned request. func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metrics.Labels, hookExecutor hookexecution.HookStageExecutor) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImpId stored_responses.BidderImpReplaceImpID, account *config.Account, errs []error) { - req = &openrtb_ext.RequestWrapper{} - req.BidRequest = &openrtb2.BidRequest{} errs = nil - - // Pull the request body into a buffer, so we have it for later usage. - lr := &io.LimitedReader{ - R: httpRequest.Body, + var err error + var r io.ReadCloser = httpRequest.Body + reqContentEncoding := httputil.ContentEncoding(httpRequest.Header.Get("Content-Encoding")) + if reqContentEncoding != "" { + if !deps.cfg.Compression.Request.IsSupported(reqContentEncoding) { + errs = []error{fmt.Errorf("Content-Encoding of type %s is not supported", reqContentEncoding)} + return + } else { + r, err = getCompressionEnabledReader(httpRequest.Body, reqContentEncoding) + if err != nil { + errs = []error{err} + return + } + } + } + defer r.Close() + limitedReqReader := &io.LimitedReader{ + R: r, N: deps.cfg.MaxRequestSize, } - requestJson, err := io.ReadAll(lr) + + requestJson, err := io.ReadAll(limitedReqReader) if err != nil { errs = []error{err} return } - // If the request size was too large, read through the rest of the request body so that the connection can be reused. - if lr.N <= 0 { - if written, err := io.Copy(io.Discard, httpRequest.Body); written > 0 || err != nil { - errs = []error{fmt.Errorf("Request size exceeded max size of %d bytes.", deps.cfg.MaxRequestSize)} + + if limitedReqReader.N <= 0 { + // Limited Reader returns 0 if the request was exactly at the max size or over the limit. + // This is because it only reads up to N bytes. To check if the request was too large, + // we need to look at the next byte of its underlying reader, limitedReader.R. + if _, err := limitedReqReader.R.Read(make([]byte, 1)); err != io.EOF { + // Discard the rest of the request body so that the connection can be reused. + io.Copy(io.Discard, httpRequest.Body) + errs = []error{fmt.Errorf("request size exceeded max size of %d bytes.", deps.cfg.MaxRequestSize)} return } } + req = &openrtb_ext.RequestWrapper{} + req.BidRequest = &openrtb2.BidRequest{} + requestJson, rejectErr := hookExecutor.ExecuteEntrypointStage(httpRequest, requestJson) if rejectErr != nil { errs = []error{rejectErr} @@ -463,6 +551,15 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric return } +func getCompressionEnabledReader(body io.ReadCloser, contentEncoding httputil.ContentEncoding) (io.ReadCloser, error) { + switch contentEncoding { + case httputil.ContentEncodingGZIP: + return gzip.NewReader(body) + default: + return nil, fmt.Errorf("unsupported compression type '%s'", contentEncoding) + } +} + // hasPayloadUpdatesAt checks if there are any successful payload updates at given stage func hasPayloadUpdatesAt(stageName string, outcomes []hookexecution.StageOutcome) bool { for _, outcome := range outcomes { @@ -719,7 +816,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp } if errs := validateRequestExt(req); len(errs) != 0 { - return append(errL, errs...) + if errortypes.ContainsFatalError(errs) { + return append(errL, errs...) + } + errL = append(errL, errs...) } if err := deps.validateSite(req); err != nil { @@ -1550,6 +1650,15 @@ func validateRequestExt(req *openrtb_ext.RequestWrapper) []error { reqExt.SetPrebid(prebid) } + if !bidadjustment.Validate(prebid.BidAdjustments) { + prebid.BidAdjustments = nil + reqExt.SetPrebid(prebid) + errs = append(errs, &errortypes.Warning{ + WarningCode: errortypes.BidAdjustmentWarningCode, + Message: "bid adjustment from request was invalid", + }) + } + return errs } @@ -1563,29 +1672,50 @@ func validateTargeting(t *openrtb_ext.ExtRequestTargeting) error { } if t.PriceGranularity != nil { - pg := t.PriceGranularity + if err := validatePriceGranularity(t.PriceGranularity); err != nil { + return err + } + } - if pg.Precision == nil { - return errors.New("Price granularity error: precision is required") - } else if *pg.Precision < 0 { - return errors.New("Price granularity error: precision must be non-negative") - } else if *pg.Precision > openrtb_ext.MaxDecimalFigures { - return fmt.Errorf("Price granularity error: precision of more than %d significant figures is not supported", openrtb_ext.MaxDecimalFigures) + if t.MediaTypePriceGranularity.Video != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Video); err != nil { + return err + } + } + if t.MediaTypePriceGranularity.Banner != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Banner); err != nil { + return err + } + } + if t.MediaTypePriceGranularity.Native != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Native); err != nil { + return err } + } - var prevMax float64 = 0 - for _, gr := range pg.Ranges { - if gr.Max <= prevMax { - return errors.New(`Price granularity error: range list must be ordered with increasing "max"`) - } + return nil +} - if gr.Increment <= 0.0 { - return errors.New("Price granularity error: increment must be a nonzero positive number") - } - prevMax = gr.Max - } +func validatePriceGranularity(pg *openrtb_ext.PriceGranularity) error { + if pg.Precision == nil { + return errors.New("Price granularity error: precision is required") + } else if *pg.Precision < 0 { + return errors.New("Price granularity error: precision must be non-negative") + } else if *pg.Precision > openrtb_ext.MaxDecimalFigures { + return fmt.Errorf("Price granularity error: precision of more than %d significant figures is not supported", openrtb_ext.MaxDecimalFigures) } + var prevMax float64 = 0 + for _, gr := range pg.Ranges { + if gr.Max <= prevMax { + return errors.New(`Price granularity error: range list must be ordered with increasing "max"`) + } + + if gr.Increment <= 0.0 { + return errors.New("Price granularity error: increment must be a nonzero positive number") + } + prevMax = gr.Max + } return nil } @@ -1666,11 +1796,9 @@ func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases // Check Universal User ID eids := userExt.GetEid() if eids != nil { - if len(*eids) == 0 { - return append(errL, errors.New("request.user.ext.eids must contain at least one element or be undefined")) - } - uniqueSources := make(map[string]struct{}, len(*eids)) - for eidIndex, eid := range *eids { + eidsValue := *eids + uniqueSources := make(map[string]struct{}, len(eidsValue)) + for eidIndex, eid := range eidsValue { if eid.Source == "" { return append(errL, fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex)) } @@ -1706,6 +1834,7 @@ func validateRegs(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) []er for _, id := range req.BidRequest.Regs.GPPSID { if id == int8(constants.SectionTCFEU2) { gdpr = 1 + break } } if gdpr != *req.BidRequest.Regs.GDPR { @@ -1838,8 +1967,12 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) { } func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { + if r.Site == nil { + r.Site = &openrtb2.Site{} + } + referrerCandidate := httpReq.Referer() - if referrerCandidate == "" && r.Site != nil && r.Site.Page != "" { + if referrerCandidate == "" && r.Site.Page != "" { referrerCandidate = r.Site.Page // If http referer is disabled and thus has empty value - use site.page instead } @@ -1853,17 +1986,13 @@ func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { } } - if r.Site != nil { - if siteExt, err := r.GetSiteExt(); err == nil && siteExt.GetAmp() == nil { - siteExt.SetAmp(¬Amp) - } + if siteExt, err := r.GetSiteExt(); err == nil && siteExt.GetAmp() == nil { + siteExt.SetAmp(¬Amp) } + } func setSitePageIfEmpty(site *openrtb2.Site, sitePage string) { - if site == nil { - site = &openrtb2.Site{} - } if site.Page == "" { site.Page = sitePage } diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go index 15d70efa5a9..a3c19ee0b10 100644 --- a/endpoints/openrtb2/auction_benchmark_test.go +++ b/endpoints/openrtb2/auction_benchmark_test.go @@ -15,8 +15,8 @@ import ( "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/experiment/adscert" - "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/hooks" + "github.com/prebid/prebid-server/macros" metricsConfig "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" @@ -82,9 +82,6 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { gdprPermsBuilder := fakePermissionsBuilder{ permissions: &fakePermissions{}, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder exchange := exchange.NewExchange( adapters, @@ -94,10 +91,10 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { nilMetrics, infos, gdprPermsBuilder, - tcf2ConfigBuilder, currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), empty_fetcher.EmptyFetcher{}, &adscert.NilSigner{}, + macros.NewStringIndexBasedReplacer(), ) endpoint, _ := NewEndpoint( @@ -114,6 +111,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { nil, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) b.ResetTimer() @@ -142,7 +140,7 @@ func BenchmarkValidWholeExemplary(b *testing.B) { if err != nil { b.Fatalf("unable to read file %s", testFile) } - test, err := parseTestFile(fileData, testFile) + test, err := parseTestData(fileData, testFile) if err != nil { b.Fatal(err.Error()) } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 8eda4de9b1d..84e5cf4baf5 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -2,10 +2,12 @@ package openrtb2 import ( "bytes" + "compress/gzip" "context" "encoding/json" "errors" "io" + "io/fs" "net" "net/http" "net/http/httptest" @@ -21,15 +23,13 @@ import ( nativeRequests "github.com/prebid/openrtb/v19/native1/request" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/analytics" - "github.com/prebid/prebid-server/hooks" - "github.com/prebid/prebid-server/hooks/hookexecution" - "github.com/prebid/prebid-server/hooks/hookstage" - "github.com/stretchr/testify/assert" - analyticsConf "github.com/prebid/prebid-server/analytics/config" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/exchange" + "github.com/prebid/prebid-server/hooks" + "github.com/prebid/prebid-server/hooks/hookexecution" + "github.com/prebid/prebid-server/hooks/hookstage" "github.com/prebid/prebid-server/metrics" metricsConfig "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -37,8 +37,11 @@ import ( "github.com/prebid/prebid-server/stored_responses" "github.com/prebid/prebid-server/util/iputil" "github.com/prebid/prebid-server/util/ptrutil" + "github.com/stretchr/testify/assert" ) +const jsonFileExtension string = ".json" + func TestJsonSampleRequests(t *testing.T) { testSuites := []struct { description string @@ -112,49 +115,71 @@ func TestJsonSampleRequests(t *testing.T) { "Assert request with ad server targeting is processing correctly", "adservertargeting", }, + { + "Assert request with bid adjustments defined is processing correctly", + "bidadjustments", + }, } for _, tc := range testSuites { - testCaseFiles, err := getTestFiles(filepath.Join("sample-requests", tc.sampleRequestsSubDir)) - if assert.NoError(t, err, "Test case %s. Error reading files from directory %s \n", tc.description, tc.sampleRequestsSubDir) { - for _, testFile := range testCaseFiles { - fileData, err := os.ReadFile(testFile) - if assert.NoError(t, err, "Test case %s. Error reading file %s \n", tc.description, testFile) { - // Retrieve test case input and expected output from JSON file - test, err := parseTestFile(fileData, testFile) - if !assert.NoError(t, err) { - continue - } + err := filepath.WalkDir(filepath.Join("sample-requests", tc.sampleRequestsSubDir), func(path string, info fs.DirEntry, err error) error { + // According to documentation, needed to avoid panics + if err != nil { + return err + } - // Build endpoint for testing. If no error, run test case - cfg := &config.Configuration{MaxRequestSize: maxSize} - if test.Config != nil { - cfg.BlacklistedApps = test.Config.BlacklistedApps - cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap() - cfg.BlacklistedAccts = test.Config.BlacklistedAccounts - cfg.BlacklistedAcctMap = test.Config.getBlackListedAccountMap() - cfg.AccountRequired = test.Config.AccountRequired - } - cfg.MarshalAccountDefaults() - test.endpointType = OPENRTB_ENDPOINT + // Test suite will traverse the directory tree recursively and will only consider files with `json` extension + if !info.IsDir() && filepath.Ext(info.Name()) == jsonFileExtension { + t.Run(tc.description, func(t *testing.T) { + runJsonBasedTest(t, path, tc.description) + }) + } - auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) - if assert.NoError(t, err) { - assert.NotPanics(t, func() { runTestCase(t, auctionEndpointHandler, test, fileData, testFile) }, testFile) - } + return nil + }) + assert.NoError(t, err, "Test case %s. Error reading files from directory %s \n", tc.description, tc.sampleRequestsSubDir) + } +} - // Close servers regardless if the test case was run or not - for _, mockBidServer := range mockBidServers { - mockBidServer.Close() - } - mockCurrencyRatesServer.Close() - } - } - } +func runJsonBasedTest(t *testing.T, filename, desc string) { + t.Helper() + + fileData, err := os.ReadFile(filename) + if !assert.NoError(t, err, "Test case %s. Error reading file %s \n", desc, filename) { + return + } + + // Retrieve test case input and expected output from JSON file + test, err := parseTestData(fileData, filename) + if !assert.NoError(t, err) { + return + } + + // Build endpoint for testing. If no error, run test case + cfg := &config.Configuration{MaxRequestSize: maxSize} + if test.Config != nil { + cfg.BlacklistedApps = test.Config.BlacklistedApps + cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap() + cfg.BlacklistedAccts = test.Config.BlacklistedAccounts + cfg.BlacklistedAcctMap = test.Config.getBlackListedAccountMap() + cfg.AccountRequired = test.Config.AccountRequired + } + cfg.MarshalAccountDefaults() + test.endpointType = OPENRTB_ENDPOINT + + auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) + if assert.NoError(t, err) { + assert.NotPanics(t, func() { runEndToEndTest(t, auctionEndpointHandler, test, fileData, filename) }, filename) + } + + // Close servers regardless if the test case was run or not + for _, mockBidServer := range mockBidServers { + mockBidServer.Close() } + mockCurrencyRatesServer.Close() } -func runTestCase(t *testing.T, auctionEndpointHandler httprouter.Handle, test testCase, fileData []byte, testFile string) { +func runEndToEndTest(t *testing.T, auctionEndpointHandler httprouter.Handle, test testCase, fileData []byte, testFile string) { t.Helper() // Hit the auction endpoint with the test case configuration and mockBidRequest @@ -429,7 +454,9 @@ func TestExplicitUserId(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) endpoint(httptest.NewRecorder(), request, nil) @@ -468,7 +495,7 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) { bidderInfos := getBidderInfos(nil, openrtb_ext.CoreBidderNames()) bidderMap := exchange.GetActiveBidders(bidderInfos) - disabledBidders := exchange.GetDisabledBiddersErrorMessages(bidderInfos) + disabledBidders := exchange.GetDisabledBidderWarningMessages(bidderInfos) // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. @@ -485,7 +512,9 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) { aliasJSON, bidderMap, empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(testBidRequest)) recorder := httptest.NewRecorder() @@ -537,7 +566,9 @@ func TestNilExchange(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil Exchange.") @@ -561,7 +592,9 @@ func TestNilValidator(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil BidderParamValidator.") @@ -585,7 +618,9 @@ func TestExchangeError(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() @@ -710,7 +745,9 @@ func TestImplicitIPsEndToEnd(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) httpReq.Header.Set("X-Forwarded-For", test.xForwardedForHeader) @@ -908,7 +945,9 @@ func TestImplicitDNTEndToEnd(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) httpReq.Header.Set("DNT", test.dntHeader) @@ -926,19 +965,22 @@ func TestImplicitDNTEndToEnd(t *testing.T) { func TestReferer(t *testing.T) { testCases := []struct { description string - givenSitePage string - givenSiteDomain string - givenPublisherDomain string givenReferer string expectedDomain string expectedPage string expectedPublisherDomain string + bidReq *openrtb_ext.RequestWrapper }{ { description: "site.page/domain are unchanged when site.page/domain and http referer are not set", expectedDomain: "", expectedPage: "", expectedPublisherDomain: "", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{}, + }, + }}, }, { description: "site.page/domain are derived from referer when neither is set and http referer is set", @@ -946,39 +988,73 @@ func TestReferer(t *testing.T) { expectedDomain: "test.somepage.com", expectedPublisherDomain: "somepage.com", expectedPage: "https://test.somepage.com", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{}, + }, + }}, }, { description: "site.domain is derived from site.page when site.page is set and http referer is not set", - givenSitePage: "https://test.somepage.com", expectedDomain: "test.somepage.com", expectedPublisherDomain: "somepage.com", expectedPage: "https://test.somepage.com", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Page: "https://test.somepage.com", + Publisher: &openrtb2.Publisher{}, + }, + }}, }, { description: "site.domain is derived from http referer when site.page and http referer are set", - givenSitePage: "https://test.somepage.com", givenReferer: "http://test.com", expectedDomain: "test.com", expectedPublisherDomain: "test.com", expectedPage: "https://test.somepage.com", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Page: "https://test.somepage.com", + Publisher: &openrtb2.Publisher{}, + }, + }}, }, { description: "site.page/domain are unchanged when site.page/domain and http referer are set", - givenSitePage: "https://test.somepage.com", - givenSiteDomain: "some.domain.com", givenReferer: "http://test.com", expectedDomain: "some.domain.com", expectedPublisherDomain: "test.com", expectedPage: "https://test.somepage.com", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "some.domain.com", + Page: "https://test.somepage.com", + Publisher: &openrtb2.Publisher{}, + }, + }}, }, { description: "Publisher domain shouldn't be overrwriten if already set", - givenSitePage: "https://test.somepage.com", - givenSiteDomain: "", - givenPublisherDomain: "differentpage.com", expectedDomain: "test.somepage.com", expectedPublisherDomain: "differentpage.com", expectedPage: "https://test.somepage.com", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "", + Page: "https://test.somepage.com", + Publisher: &openrtb2.Publisher{ + Domain: "differentpage.com", + }, + }, + }}, + }, + { + description: "request.site is nil", + givenReferer: "http://test.com", + expectedDomain: "test.com", + expectedPublisherDomain: "test.com", + expectedPage: "http://test.com", + bidReq: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, }, } @@ -986,20 +1062,12 @@ func TestReferer(t *testing.T) { httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) httpReq.Header.Set("Referer", test.givenReferer) - bidReq := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ - Site: &openrtb2.Site{ - Domain: test.givenSiteDomain, - Page: test.givenSitePage, - Publisher: &openrtb2.Publisher{Domain: test.givenPublisherDomain}, - }, - }} - - setSiteImplicitly(httpReq, bidReq) + setSiteImplicitly(httpReq, test.bidReq) - assert.NotNil(t, bidReq.Site, test.description) - assert.Equal(t, test.expectedDomain, bidReq.Site.Domain, test.description) - assert.Equal(t, test.expectedPage, bidReq.Site.Page, test.description) - assert.Equal(t, test.expectedPublisherDomain, bidReq.Site.Publisher.Domain, test.description) + assert.NotNil(t, test.bidReq.Site, test.description) + assert.Equal(t, test.expectedDomain, test.bidReq.Site.Domain, test.description) + assert.Equal(t, test.expectedPage, test.bidReq.Site.Page, test.description) + assert.Equal(t, test.expectedPublisherDomain, test.bidReq.Site.Publisher.Domain, test.description) } } @@ -1119,6 +1187,7 @@ func TestStoredRequests(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } testStoreVideoAttr := []bool{true, true, false, false, false} @@ -1492,6 +1561,7 @@ func TestValidateRequest(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } testCases := []struct { @@ -1586,7 +1656,7 @@ func TestValidateRequest(t *testing.T) { Ext: []byte(`{"appnexus":{"placementId": 12345678}}`), }, }, - Ext: []byte(`{"prebid":{"aliases":{"brightroll":"appnexus"}, "aliasgvlids":{"pubmatic1":1}}}`), + Ext: []byte(`{"prebid":{"aliases":{"yahoossp":"appnexus"}, "aliasgvlids":{"pubmatic1":1}}}`), }, }, givenIsAmp: false, @@ -1617,11 +1687,11 @@ func TestValidateRequest(t *testing.T) { Ext: []byte(`{"appnexus":{"placementId": 12345678}}`), }, }, - Ext: []byte(`{"prebid":{"aliases":{"brightroll":"appnexus"}, "aliasgvlids":{"brightroll":0}}}`), + Ext: []byte(`{"prebid":{"aliases":{"yahoossp":"appnexus"}, "aliasgvlids":{"yahoossp":0}}}`), }, }, givenIsAmp: false, - expectedErrorList: []error{errors.New("request.ext.prebid.aliasgvlids. Invalid vendorId 0 for alias: brightroll. Choose a different vendorId, or remove this entry.")}, + expectedErrorList: []error{errors.New("request.ext.prebid.aliasgvlids. Invalid vendorId 0 for alias: yahoossp. Choose a different vendorId, or remove this entry.")}, expectedChannelObject: &openrtb_ext.ExtRequestPrebidChannel{Name: appChannel, Version: ""}, }, { @@ -1648,7 +1718,7 @@ func TestValidateRequest(t *testing.T) { Ext: []byte(`{"appnexus":{"placementId": 12345678}}`), }, }, - Ext: []byte(`{"prebid":{"aliases":{"brightroll":"appnexus"}, "aliasgvlids":{"brightroll":1}}}`), + Ext: []byte(`{"prebid":{"aliases":{"yahoossp":"appnexus"}, "aliasgvlids":{"yahoossp":1}}}`), }, }, givenIsAmp: false, @@ -1836,60 +1906,172 @@ func TestValidateTargeting(t *testing.T) { expectedError: nil, }, { - name: "price granularity empty", + name: "price granularity ranges out of order", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - PriceGranularity: &openrtb_ext.PriceGranularity{}, + IncludeWinners: ptrutil.ToPtr(true), + PriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 1.0, Max: 2.0, Increment: 0.2}, + {Min: 0.0, Max: 1.0, Increment: 0.5}, + }, + }, }, - expectedError: errors.New("Price granularity error: precision is required"), + expectedError: errors.New(`Price granularity error: range list must be ordered with increasing "max"`), }, { - name: "price granularity negative", + name: "media type price granularity video correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ IncludeWinners: ptrutil.ToPtr(true), - PriceGranularity: &openrtb_ext.PriceGranularity{ - Precision: ptrutil.ToPtr(-1), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: 1}, + }, + }, }, }, - expectedError: errors.New("Price granularity error: precision must be non-negative"), + expectedError: nil, }, { - name: "price granularity greater than max", + name: "media type price granularity banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ IncludeWinners: ptrutil.ToPtr(true), - PriceGranularity: &openrtb_ext.PriceGranularity{ - Precision: ptrutil.ToPtr(openrtb_ext.MaxDecimalFigures + 1), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: 1}, + }, + }, }, }, - expectedError: errors.New("Price granularity error: precision of more than 15 significant figures is not supported"), + expectedError: nil, }, { - name: "price granularity ranges out of order", + name: "media type price granularity native correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ IncludeWinners: ptrutil.ToPtr(true), - PriceGranularity: &openrtb_ext.PriceGranularity{ - Precision: ptrutil.ToPtr(2), - Ranges: []openrtb_ext.GranularityRange{ - {Min: 1.0, Max: 2.0, Increment: 0.2}, - {Min: 0.0, Max: 1.0, Increment: 0.5}, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 20.0, Increment: 1}, + }, }, }, }, - expectedError: errors.New(`Price granularity error: range list must be ordered with increasing "max"`), + expectedError: nil, }, { - name: "price granularity negative increment", + name: "media type price granularity video and banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ IncludeWinners: ptrutil.ToPtr(true), - PriceGranularity: &openrtb_ext.PriceGranularity{ - Precision: ptrutil.ToPtr(2), - Ranges: []openrtb_ext.GranularityRange{ - {Min: 0.0, Max: 1.0, Increment: -0.1}, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: 1}, + }, + }, + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: 1}, + }, + }, + }, + }, + expectedError: nil, + }, + { + name: "media type price granularity video incorrect", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + IncludeWinners: ptrutil.ToPtr(true), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: -1}, + }, }, }, }, expectedError: errors.New("Price granularity error: increment must be a nonzero positive number"), }, + { + name: "media type price granularity banner incorrect", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + IncludeWinners: ptrutil.ToPtr(true), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 0.0, Increment: 1}, + }, + }, + }, + }, + expectedError: errors.New("Price granularity error: range list must be ordered with increasing \"max\""), + }, + { + name: "media type price granularity native incorrect", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + IncludeWinners: ptrutil.ToPtr(true), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 0.0, Increment: 1}, + }, + }, + }, + }, + expectedError: errors.New("Price granularity error: range list must be ordered with increasing \"max\""), + }, + { + name: "media type price granularity video correct and banner incorrect", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + IncludeWinners: ptrutil.ToPtr(true), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: -1}, + }, + }, + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 0.0, Increment: 1}, + }, + }, + }, + }, + expectedError: errors.New("Price granularity error: range list must be ordered with increasing \"max\""), + }, + { + name: "media type price granularity native incorrect and banner correct", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + IncludeWinners: ptrutil.ToPtr(true), + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: -1}, + }, + }, + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 0.0, Increment: 1}, + }, + }, + }, + }, + expectedError: errors.New("Price granularity error: range list must be ordered with increasing \"max\""), + }, } for _, tc := range testCases { @@ -1899,6 +2081,80 @@ func TestValidateTargeting(t *testing.T) { } } +func TestValidatePriceGranularity(t *testing.T) { + testCases := []struct { + description string + givenPriceGranularity *openrtb_ext.PriceGranularity + expectedError error + }{ + { + description: "Precision is nil", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: nil, + }, + expectedError: errors.New("Price granularity error: precision is required"), + }, + { + description: "Precision is negative", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(-1), + }, + expectedError: errors.New("Price granularity error: precision must be non-negative"), + }, + { + description: "Precision is too big", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(20), + }, + expectedError: errors.New("Price granularity error: precision of more than 15 significant figures is not supported"), + }, + { + description: "price granularity ranges out of order", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 1.0, Max: 2.0, Increment: 0.2}, + {Min: 0.0, Max: 1.0, Increment: 0.5}, + }, + }, + expectedError: errors.New(`Price granularity error: range list must be ordered with increasing "max"`), + }, + { + description: "price granularity negative increment", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 1.0, Increment: -0.1}, + }, + }, + expectedError: errors.New("Price granularity error: increment must be a nonzero positive number"), + }, + { + description: "price granularity correct", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []openrtb_ext.GranularityRange{ + {Min: 0.0, Max: 10.0, Increment: 1}, + }, + }, + expectedError: nil, + }, + { + description: "price granularity with correct precision and ranges not specified", + givenPriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(2), + }, + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + assert.Equal(t, tc.expectedError, validatePriceGranularity(tc.givenPriceGranularity)) + }) + } +} + func TestValidateOrFillChannel(t *testing.T) { testCases := []struct { description string @@ -2024,6 +2280,7 @@ func TestSetIntegrationType(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } testCases := []struct { @@ -2089,6 +2346,7 @@ func TestStoredRequestGenerateUuid(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } req := &openrtb2.BidRequest{} @@ -2192,6 +2450,7 @@ func TestOversizedRequest(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -2230,6 +2489,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -2262,6 +2522,7 @@ func TestNoEncoding(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() @@ -2346,6 +2607,7 @@ func TestContentType(t *testing.T) { openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, ) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() @@ -2566,6 +2828,7 @@ func TestValidateImpExt(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } for _, group := range testGroups { @@ -2618,6 +2881,7 @@ func TestCurrencyTrunc(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } ui := int64(1) @@ -2665,6 +2929,7 @@ func TestCCPAInvalid(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } ui := int64(1) @@ -2716,6 +2981,7 @@ func TestNoSaleInvalid(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } ui := int64(1) @@ -2770,6 +3036,7 @@ func TestValidateSourceTID(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } ui := int64(1) @@ -2814,6 +3081,7 @@ func TestSChainInvalid(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } ui := int64(1) @@ -3309,6 +3577,7 @@ func TestEidPermissionsInvalid(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } ui := int64(1) @@ -3557,7 +3826,9 @@ func TestIOS14EndToEnd(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "app-ios140-no-ifa.json"))) @@ -3588,6 +3859,11 @@ func TestAuctionWarnings(t *testing.T) { file: "us-privacy-conflict.json", expectedWarning: "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp", }, + { + name: "empty-gppsid-array-conflicts-with-regs-gdpr", // gdpr set to 1, an empty non-nil gpp_sid array doesn't match + file: "empty-gppsid-conflict.json", + expectedWarning: "regs.gdpr signal conflicts with GPP (regs.gpp_sid) and will be ignored", + }, { name: "gdpr-signals-conflict", // gdpr signals do not match file: "gdpr-conflict.json", @@ -3618,6 +3894,7 @@ func TestAuctionWarnings(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { @@ -3663,6 +3940,7 @@ func TestParseRequestParseImpInfoError(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) @@ -3677,6 +3955,106 @@ func TestParseRequestParseImpInfoError(t *testing.T) { assert.Contains(t, errL[0].Error(), "echovideoattrs of type bool", "Incorrect error message") } +func TestParseGzipedRequest(t *testing.T) { + testCases := + []struct { + desc string + reqContentEnc string + maxReqSize int64 + compressionCfg config.Compression + expectedErr string + }{ + { + desc: "Gzip compression enabled, request size exceeds max request size", + reqContentEnc: "gzip", + maxReqSize: 10, + compressionCfg: config.Compression{Request: config.CompressionInfo{GZIP: true}}, + expectedErr: "request size exceeded max size of 10 bytes.", + }, + { + desc: "Gzip compression enabled, request size is within max request size", + reqContentEnc: "gzip", + maxReqSize: 2000, + compressionCfg: config.Compression{Request: config.CompressionInfo{GZIP: true}}, + expectedErr: "", + }, + { + desc: "Gzip compression enabled, request size is within max request size, content-encoding value not in lower case", + reqContentEnc: "GZIP", + maxReqSize: 2000, + compressionCfg: config.Compression{Request: config.CompressionInfo{GZIP: true}}, + expectedErr: "", + }, + { + desc: "Request is Gzip compressed, but Gzip compression is disabled", + reqContentEnc: "gzip", + compressionCfg: config.Compression{Request: config.CompressionInfo{GZIP: false}}, + expectedErr: "Content-Encoding of type gzip is not supported", + }, + { + desc: "Request is not Gzip compressed, but Gzip compression is enabled", + reqContentEnc: "", + maxReqSize: 2000, + compressionCfg: config.Compression{Request: config.CompressionInfo{GZIP: true}}, + expectedErr: "", + }, + } + + reqBody := []byte(validRequest(t, "site.json")) + deps := &endpointDeps{ + fakeUUIDGenerator{}, + &warningsCheckExchange{}, + mockBidderParamValidator{}, + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: int64(50), Compression: config.Compression{Request: config.CompressionInfo{GZIP: false}}}, + &metricsConfig.NilMetricsEngine{}, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + } + + hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) + for _, test := range testCases { + var req *http.Request + deps.cfg.MaxRequestSize = test.maxReqSize + deps.cfg.Compression = test.compressionCfg + if test.reqContentEnc == "gzip" { + var compressed bytes.Buffer + gw := gzip.NewWriter(&compressed) + _, err := gw.Write(reqBody) + assert.NoError(t, err, "Error writing gzip compressed request body", test.desc) + assert.NoError(t, gw.Close(), "Error closing gzip writer", test.desc) + + req = httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(compressed.Bytes())) + req.Header.Set("Content-Encoding", "gzip") + } else { + req = httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(reqBody)) + } + resReq, impExtInfoMap, _, _, _, _, errL := deps.parseRequest(req, &metrics.Labels{}, hookExecutor) + + if test.expectedErr == "" { + assert.Nil(t, errL, "Error list should be nil", test.desc) + assert.NotNil(t, resReq, "Result request should not be nil", test.desc) + assert.NotNil(t, impExtInfoMap, "Impression info map should not be nil", test.desc) + } else { + assert.Nil(t, resReq, "Result request should be nil due to incorrect imp", test.desc) + assert.Nil(t, impExtInfoMap, "Impression info map should be nil due to incorrect imp", test.desc) + assert.Len(t, errL, 1, "One error should be returned", test.desc) + assert.Contains(t, errL[0].Error(), test.expectedErr, "Incorrect error message", test.desc) + } + } +} + func TestValidateNativeContextTypes(t *testing.T) { impIndex := 4 @@ -4136,7 +4514,9 @@ func TestAuctionResponseHeaders(t *testing.T) { []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}) + hooks.EmptyPlanBuilder{}, + nil, + ) for _, test := range testCases { httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(test.requestBody)) @@ -4240,6 +4620,7 @@ func TestParseRequestMergeBidderParams(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) @@ -4305,13 +4686,10 @@ func TestParseRequestStoredResponses(t *testing.T) { expectedErrorCount: 0, }, { - name: "req has two imps with missing stored responses", - givenRequestBody: validRequest(t, "req-two-imps-missing-stored-response.json"), - expectedStoredResponses: map[string]json.RawMessage{ - "imp-id1": json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "appnexus"}]`), - "imp-id2": json.RawMessage(nil), - }, - expectedErrorCount: 0, + name: "req has two imps with missing stored responses", + givenRequestBody: validRequest(t, "req-two-imps-missing-stored-response.json"), + expectedStoredResponses: nil, + expectedErrorCount: 2, }, { name: "req has two imps: one with stored response and another imp without stored resp", @@ -4345,6 +4723,7 @@ func TestParseRequestStoredResponses(t *testing.T) { hardcodedResponseIPValidator{response: true}, &mockStoredResponseFetcher{mockStoredResponses}, hooks.EmptyPlanBuilder{}, + nil, } hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) @@ -4404,13 +4783,10 @@ func TestParseRequestStoredBidResponses(t *testing.T) { expectedErrorCount: 0, }, { - name: "req has two imps with missing stored bid responses", - givenRequestBody: validRequest(t, "req-two-imps-missing-stored-bid-response.json"), - expectedStoredBidResponses: map[string]map[string]json.RawMessage{ - "imp-id1": {"testBidder1": nil}, - "imp-id2": {"testBidder2": nil}, - }, - expectedErrorCount: 0, + name: "req has two imps with missing stored bid responses", + givenRequestBody: validRequest(t, "req-two-imps-missing-stored-bid-response.json"), + expectedStoredBidResponses: nil, + expectedErrorCount: 2, }, } for _, test := range tests { @@ -4435,6 +4811,7 @@ func TestParseRequestStoredBidResponses(t *testing.T) { hardcodedResponseIPValidator{response: true}, &mockStoredResponseFetcher{mockStoredBidResponses}, hooks.EmptyPlanBuilder{}, + nil, } hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) @@ -4471,6 +4848,7 @@ func TestValidateStoredResp(t *testing.T) { hardcodedResponseIPValidator{response: true}, &mockStoredResponseFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } testCases := []struct { @@ -5058,28 +5436,33 @@ func TestValidResponseAfterExecutingStages(t *testing.T) { { description: "Assert correct BidResponse when request rejected at entrypoint stage", file: "sample-requests/hooks/auction_entrypoint_reject.json", - planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr, nil})}, }, { description: "Assert correct BidResponse when request rejected at raw-auction stage", file: "sample-requests/hooks/auction_raw_auction_request_reject.json", - planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr, nil})}, }, { description: "Assert correct BidResponse when request rejected at processed-auction stage", file: "sample-requests/hooks/auction_processed_auction_request_reject.json", - planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr, nil})}, }, { // bidder-request stage doesn't reject whole request, so we do not expect NBR code in response description: "Assert correct BidResponse when request rejected at bidder-request stage", file: "sample-requests/hooks/auction_bidder_reject.json", - planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr, nil})}, }, { description: "Assert correct BidResponse when request rejected at raw-bidder-response stage", file: "sample-requests/hooks/auction_bidder_response_reject.json", - planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr})}, + planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr, nil})}, + }, + { + description: "Assert correct BidResponse when request rejected with error from hook", + file: "sample-requests/hooks/auction_reject_with_error.json", + planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr, errors.New("dummy")})}, }, { description: "Assert correct BidResponse with debug information from modules added to ext.prebid.modules", @@ -5093,7 +5476,7 @@ func TestValidResponseAfterExecutingStages(t *testing.T) { fileData, err := os.ReadFile(tc.file) assert.NoError(t, err, "Failed to read test file.") - test, err := parseTestFile(fileData, tc.file) + test, err := parseTestData(fileData, tc.file) assert.NoError(t, err, "Failed to parse test file.") test.planBuilder = tc.planBuilder test.endpointType = OPENRTB_ENDPOINT @@ -5278,6 +5661,7 @@ func TestParseRequestMultiBid(t *testing.T) { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) @@ -5341,3 +5725,49 @@ type mockStageExecutor struct { func (e mockStageExecutor) GetOutcomes() []hookexecution.StageOutcome { return e.outcomes } + +func TestSetSeatNonBidRaw(t *testing.T) { + type args struct { + request *openrtb_ext.RequestWrapper + auctionResponse *exchange.AuctionResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "nil-auctionResponse", + args: args{auctionResponse: nil}, + wantErr: false, + }, + { + name: "nil-bidResponse", + args: args{auctionResponse: &exchange.AuctionResponse{BidResponse: nil}}, + wantErr: false, + }, + { + name: "invalid-response.Ext", + args: args{auctionResponse: &exchange.AuctionResponse{BidResponse: &openrtb2.BidResponse{Ext: []byte(`invalid_json`)}}}, + wantErr: true, + }, + { + name: "update-seatnonbid-in-ext", + args: args{ + request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": { "returnallbidstatus" : true }}`)}}, + auctionResponse: &exchange.AuctionResponse{ + ExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: &openrtb_ext.ExtResponsePrebid{SeatNonBid: []openrtb_ext.SeatNonBid{}}}, + BidResponse: &openrtb2.BidResponse{Ext: []byte(`{}`)}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := setSeatNonBidRaw(tt.args.request, tt.args.auctionResponse); (err != nil) != tt.wantErr { + t.Errorf("setSeatNonBidRaw() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json index c775d332ecd..034d3235e15 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json @@ -105,7 +105,8 @@ ] }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "mediatypepricegranularity": {} } } } diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json index f2e2a21791b..8dc19f6f24d 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json @@ -99,7 +99,8 @@ ] }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "mediatypepricegranularity": {} } } } diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json index 1cf81ea2451..4003abf99cb 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json @@ -104,7 +104,8 @@ ] }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "mediatypepricegranularity": {} } } } diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json index c596a8328a1..c6389dadc29 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json @@ -104,7 +104,8 @@ ] }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "mediatypepricegranularity": {} } } } diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json index fdc25ed92b9..b62a745b1bf 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json @@ -104,7 +104,8 @@ ] }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "mediatypepricegranularity": {} } } } diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/cpm.json b/endpoints/openrtb2/sample-requests/bidadjustments/cpm.json new file mode 100644 index 00000000000..db0a06e6ad9 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/bidadjustments/cpm.json @@ -0,0 +1,105 @@ +{ + "description": "Bid Adjustment Test with CPM, and WildCard Preference Testing", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 20.00 + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 2.0 + } + }, + "usepbsrates": false + }, + "bidadjustments": { + "mediatype": { + "banner": { + "appnexus": { + "*": [ + { + "adjtype": "cpm", + "value": 5.0, + "currency": "EUR" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": 3.0 + } + ] + } + } + } + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 10.0, + "ext": { + "origbidcpm": 20, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/invalid.json b/endpoints/openrtb2/sample-requests/bidadjustments/invalid.json new file mode 100644 index 00000000000..cbd5f758316 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/bidadjustments/invalid.json @@ -0,0 +1,90 @@ +{ + "description": "Bid Adjustment Test With Invalid Adjustment", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 2.00, + "dealid": "some-deal-id" + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "bidadjustments": { + "mediatype": { + "banner": { + "appnexus": { + "some-deal-id": [ + { + "adjtype": "multiplier", + "value": -2.0 + } + ] + } + } + } + }, + "debug": true + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 2.0, + "ext": { + "origbidcpm": 2, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/multiplier.json b/endpoints/openrtb2/sample-requests/bidadjustments/multiplier.json new file mode 100644 index 00000000000..72a8dca9bbc --- /dev/null +++ b/endpoints/openrtb2/sample-requests/bidadjustments/multiplier.json @@ -0,0 +1,89 @@ +{ + "description": "Bid Adjustment Test With One Adjustment", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 2.00, + "dealid": "some-deal-id" + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "bidadjustments": { + "mediatype": { + "banner": { + "appnexus": { + "some-deal-id": [ + { + "adjtype": "multiplier", + "value": 2.0 + } + ] + } + } + } + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 4.0, + "ext": { + "origbidcpm": 2, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/static.json b/endpoints/openrtb2/sample-requests/bidadjustments/static.json new file mode 100644 index 00000000000..9969c47ef9b --- /dev/null +++ b/endpoints/openrtb2/sample-requests/bidadjustments/static.json @@ -0,0 +1,106 @@ +{ + "description": "Bid Adjustment with Static and WildCard testing", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 2.00, + "dealid": "some-deal-id" + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 2.0 + } + }, + "usepbsrates": false + }, + "bidadjustments": { + "mediatype": { + "banner": { + "*": { + "some-deal-id": [ + { + "adjtype": "static", + "value": 10.0, + "currency": "EUR" + } + ] + }, + "some-bidder-name": { + "*": [ + { + "adjtype": "multiplier", + "value": 2.0 + } + ] + } + } + } + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 10.0, + "ext": { + "origbidcpm": 2, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "EUR", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/hooks/auction_reject_with_error.json b/endpoints/openrtb2/sample-requests/hooks/auction_reject_with_error.json new file mode 100644 index 00000000000..5ca617a46cd --- /dev/null +++ b/endpoints/openrtb2/sample-requests/hooks/auction_reject_with_error.json @@ -0,0 +1,87 @@ +{ + "description": "Auction request", + "config": { + "mockBidders": [ + {"bidderName": "appnexus", "currency": "USD", "price": 0.00} + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "test": 1, + "ext": { + "prebid": { + "trace": "verbose" + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "bidid": "test bid id", + "nbr": 123, + "ext": { + "prebid": { + "modules": { + "errors": { + "foobar": { + "foo": [ + "dummy", + "Module foobar (hook: foo) rejected request with code 123 at entrypoint stage" + ] + } + }, + "trace": { + "stages": [ + { + "stage": "entrypoint", + "outcomes": [ + { + "entity": "http-request", + "groups": [ + { + "invocation_results": [ + { + "analytics_tags": {}, + "hook_id": { + "module_code": "foobar", + "hook_impl_code": "foo" + }, + "status": "execution_failure", + "action": "reject", + "message": "" + } + ] + } + ] + } + ] + } + ] + } + } + } + } + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json index d21ca4a94ae..8ec2273ebec 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json @@ -19,5 +19,5 @@ ] }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.site or request.app must be defined, but not both.\n" + "expectedErrorMessage": "Invalid request: request.site should include at least one of request.site.id or request.site.page.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json deleted file mode 100644 index 0baf98955b1..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "description": "Bid request with empty eids array in user.ext", - "mockBidRequest": { - "id": "anyRequestID", - "site": { - "page": "prebid.org", - "publisher": { - "id": "anyPublisher" - } - }, - "imp": [{ - "id": "anyImpID", - "ext": { - "appnexus": { - "placementId": 42 - } - }, - "banner": { - "format": [{ - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 - } - ] - } - }], - "tmax": 1000, - "user": { - "ext": { - "eids": [] - } - } - }, - "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext.eids must contain at least one element or be undefined\n" -} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json new file mode 100644 index 00000000000..9f047613277 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json @@ -0,0 +1,56 @@ +{ + "description": "Bid request with empty eids array in user.ext", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "ext": { + "appnexus": { + "placementId": 123456 + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + } + } + ], + "tmax": 1000, + "user": { + "ext": { + "eids": [] + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 0 + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json new file mode 100644 index 00000000000..9f7c923148e --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json @@ -0,0 +1,56 @@ +{ + "description": "Well formed amp request with conflicting (2.6 gdpr consent vs. GPP) gdpr signals", + "mockBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "publisher": { + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + } + }, + "source": { + "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" + }, + "tmax": 1000, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "ext": { + "appnexus": { + "placementId": 12883451 + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + } + } + ], + "regs": { + "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN", + "gpp_sid": [ ], + "gdpr": 1, + "ext": { + "us_privacy": "1YYY" + } + }, + "user": { + "consent": "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + "ext": {} + } + }, + "expectedBidResponse": { + "id":"b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json index 9e9fd538112..908e20e5b79 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json @@ -74,6 +74,7 @@ } ] }, + "mediatypepricegranularity": {}, "includewinners": true, "includebidderkeys": true }, diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json index 9f0e78be352..1e2f58cbfd2 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json @@ -76,6 +76,7 @@ } ] }, + "mediatypepricegranularity": {}, "includewinners": true, "includebidderkeys": true }, diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json index 27ee9da486c..311d4e11485 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json @@ -69,6 +69,7 @@ } ] }, + "mediatypepricegranularity": {}, "includewinners": true, "includebidderkeys": true }, diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go index 094250fa65a..3d249dc8d59 100644 --- a/endpoints/openrtb2/test_utils.go +++ b/endpoints/openrtb2/test_utils.go @@ -10,7 +10,6 @@ import ( "net/http" "net/http/httptest" "os" - "path/filepath" "strconv" "testing" "time" @@ -31,6 +30,7 @@ import ( "github.com/prebid/prebid-server/hooks" "github.com/prebid/prebid-server/hooks/hookexecution" "github.com/prebid/prebid-server/hooks/hookstage" + "github.com/prebid/prebid-server/macros" "github.com/prebid/prebid-server/metrics" metricsConfig "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -76,7 +76,7 @@ type testCase struct { ExpectedBidResponse json.RawMessage `json:"expectedBidResponse"` // "/openrtb2/amp" endpoint JSON test info - storedRequest map[string]json.RawMessage `json:"mockAmpStoredRequest"` + StoredRequest map[string]json.RawMessage `json:"mockAmpStoredRequest"` StoredResponse map[string]json.RawMessage `json:"mockAmpStoredResponse"` ExpectedAmpResponse json.RawMessage `json:"expectedAmpResponse"` } @@ -94,7 +94,7 @@ type testConfigValues struct { type brokenExchange struct{} -func (e *brokenExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (e *brokenExchange) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { return nil, errors.New("Critical, unrecoverable error.") } @@ -848,15 +848,17 @@ type mockExchange struct { lastRequest *openrtb2.BidRequest } -func (m *mockExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (m *mockExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { r := auctionRequest.BidRequestWrapper m.lastRequest = r.BidRequest - return &openrtb2.BidResponse{ - SeatBid: []openrtb2.SeatBid{{ - Bid: []openrtb2.Bid{{ - AdM: "", + return &exchange.AuctionResponse{ + BidResponse: &openrtb2.BidResponse{ + SeatBid: []openrtb2.SeatBid{{ + Bid: []openrtb2.Bid{{ + AdM: "", + }}, }}, - }}, + }, }, nil } @@ -885,8 +887,8 @@ type warningsCheckExchange struct { auctionRequest exchange.AuctionRequest } -func (e *warningsCheckExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { - e.auctionRequest = r +func (e *warningsCheckExchange) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { + e.auctionRequest = *r return nil, nil } @@ -896,13 +898,16 @@ type nobidExchange struct { gotRequest *openrtb2.BidRequest } -func (e *nobidExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (e *nobidExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { r := auctionRequest.BidRequestWrapper e.gotRequest = r.BidRequest - return &openrtb2.BidResponse{ - ID: r.BidRequest.ID, - BidID: "test bid id", - NBR: openrtb3.NoBidUnknownError.Ptr(), + + return &exchange.AuctionResponse{ + BidResponse: &openrtb2.BidResponse{ + ID: r.BidRequest.ID, + BidID: "test bid id", + NBR: openrtb3.NoBidUnknownError.Ptr(), + }, }, nil } @@ -936,6 +941,7 @@ type mockBidderHandler struct { BidderName string `json:"bidderName"` Currency string `json:"currency"` Price float64 `json:"price"` + DealID string `json:"dealid"` } func (b mockBidderHandler) bid(w http.ResponseWriter, req *http.Request) { @@ -970,9 +976,10 @@ func (b mockBidderHandler) bid(w http.ResponseWriter, req *http.Request) { { Bid: []openrtb2.Bid{ { - ID: b.BidderName + "-bid", - ImpID: openrtb2Request.Imp[0].ID, - Price: b.Price, + ID: b.BidderName + "-bid", + ImpID: openrtb2Request.Imp[0].ID, + Price: b.Price, + DealID: b.DealID, }, }, Seat: b.BidderName, @@ -1092,23 +1099,7 @@ func newBidderInfo(isDisabled bool) config.BidderInfo { } } -func getTestFiles(dir string) ([]string, error) { - var filesToAssert []string - - fileList, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - - // Append the path of every file found in `dir` to the `filesToAssert` array - for _, fileInfo := range fileList { - filesToAssert = append(filesToAssert, filepath.Join(dir, fileInfo.Name())) - } - - return filesToAssert, nil -} - -func parseTestFile(fileData []byte, testFile string) (testCase, error) { +func parseTestData(fileData []byte, testFile string) (testCase, error) { parsedTestData := testCase{} var err, errEm error @@ -1141,9 +1132,9 @@ func parseTestFile(fileData []byte, testFile string) (testCase, error) { parsedTestData.ExpectedErrorMessage, errEm = jsonparser.GetString(fileData, "expectedErrorMessage") if err == nil && errEm == nil { - return parsedTestData, errors.New("Test case file can't have both a valid expectedBidResponse and a valid expectedErrorMessage, fields are mutually exclusive") + return parsedTestData, fmt.Errorf("Test case %s can't have both a valid expectedBidResponse and a valid expectedErrorMessage, fields are mutually exclusive", testFile) } else if err != nil && errEm != nil { - return parsedTestData, errors.New("Test case file should come with either a valid expectedBidResponse or a valid expectedErrorMessage, not both.") + return parsedTestData, fmt.Errorf("Test case %s should come with either a valid expectedBidResponse or a valid expectedErrorMessage, not both.", testFile) } parsedTestData.ExpectedReturnCode = int(parsedReturnCode) @@ -1181,7 +1172,7 @@ type exchangeTestWrapper struct { actualValidatedBidReq *openrtb2.BidRequest } -func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { // rebuild/resync the request in the request wrapper. if err := r.BidRequestWrapper.RebuildRequest(); err != nil { @@ -1215,9 +1206,6 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid gdprPermsBuilder := fakePermissionsBuilder{ permissions: &fakePermissions{}, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder testExchange := exchange.NewExchange(adapterMap, &wellBehavedCache{}, @@ -1226,10 +1214,10 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid met, bidderInfos, gdprPermsBuilder, - tcf2ConfigBuilder, mockCurrencyConverter, mockFetcher, &adscert.NilSigner{}, + macros.NewStringIndexBasedReplacer(), ) testExchange = &exchangeTestWrapper{ @@ -1258,7 +1246,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han bidderInfos := getBidderInfos(test.Config.DisabledAdapters, openrtb_ext.CoreBidderNames()) bidderMap := exchange.GetActiveBidders(bidderInfos) - disabledBidders := exchange.GetDisabledBiddersErrorMessages(bidderInfos) + disabledBidders := exchange.GetDisabledBidderWarningMessages(bidderInfos) met := &metricsConfig.NilMetricsEngine{} mockFetcher := empty_fetcher.EmptyFetcher{} @@ -1277,8 +1265,8 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher) var storedRequestFetcher stored_requests.Fetcher - if len(test.storedRequest) > 0 { - storedRequestFetcher = &mockAmpStoredReqFetcher{test.storedRequest} + if len(test.StoredRequest) > 0 { + storedRequestFetcher = &mockAmpStoredReqFetcher{test.StoredRequest} } else { storedRequestFetcher = &mockStoredReqFetcher{} } @@ -1302,7 +1290,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han planBuilder = hooks.EmptyPlanBuilder{} } - var endpointBuilder func(uuidutil.UUIDGenerator, exchange.Exchange, openrtb_ext.BidderParamValidator, stored_requests.Fetcher, stored_requests.AccountFetcher, *config.Configuration, metrics.MetricsEngine, analytics.PBSAnalyticsModule, map[string]string, []byte, map[string]openrtb_ext.BidderName, stored_requests.Fetcher, hooks.ExecutionPlanBuilder) (httprouter.Handle, error) + var endpointBuilder func(uuidutil.UUIDGenerator, exchange.Exchange, openrtb_ext.BidderParamValidator, stored_requests.Fetcher, stored_requests.AccountFetcher, *config.Configuration, metrics.MetricsEngine, analytics.PBSAnalyticsModule, map[string]string, []byte, map[string]openrtb_ext.BidderName, stored_requests.Fetcher, hooks.ExecutionPlanBuilder, *exchange.TmaxAdjustmentsPreprocessed) (httprouter.Handle, error) switch test.endpointType { case AMP_ENDPOINT: @@ -1325,6 +1313,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han bidderMap, storedResponseFetcher, planBuilder, + nil, ) return endpoint, testExchange.(*exchangeTestWrapper), mockBidServersArray, mockCurrencyRatesServer, err @@ -1413,14 +1402,6 @@ func (fpb fakePermissionsBuilder) Builder(gdpr.TCF2ConfigReader, gdpr.RequestInf return fpb.permissions } -type fakeTCF2ConfigBuilder struct { - cfg gdpr.TCF2ConfigReader -} - -func (fcr fakeTCF2ConfigBuilder) Builder(hostConfig config.TCF2, accountConfig config.AccountGDPR) gdpr.TCF2ConfigReader { - return fcr.cfg -} - type fakePermissions struct { } @@ -1493,6 +1474,7 @@ func makePlan[H any](hook H) hooks.Plan[H] { type mockRejectionHook struct { nbr int + err error } func (m mockRejectionHook) HandleEntrypointHook( @@ -1500,7 +1482,7 @@ func (m mockRejectionHook) HandleEntrypointHook( _ hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload, ) (hookstage.HookResult[hookstage.EntrypointPayload], error) { - return hookstage.HookResult[hookstage.EntrypointPayload]{Reject: true, NbrCode: m.nbr}, nil + return hookstage.HookResult[hookstage.EntrypointPayload]{Reject: true, NbrCode: m.nbr}, m.err } func (m mockRejectionHook) HandleRawAuctionHook( @@ -1508,7 +1490,7 @@ func (m mockRejectionHook) HandleRawAuctionHook( _ hookstage.ModuleInvocationContext, _ hookstage.RawAuctionRequestPayload, ) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { - return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{Reject: true, NbrCode: m.nbr}, nil + return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{Reject: true, NbrCode: m.nbr}, m.err } func (m mockRejectionHook) HandleProcessedAuctionHook( @@ -1516,7 +1498,7 @@ func (m mockRejectionHook) HandleProcessedAuctionHook( _ hookstage.ModuleInvocationContext, _ hookstage.ProcessedAuctionRequestPayload, ) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) { - return hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]{Reject: true, NbrCode: m.nbr}, nil + return hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]{Reject: true, NbrCode: m.nbr}, m.err } func (m mockRejectionHook) HandleBidderRequestHook( @@ -1530,7 +1512,7 @@ func (m mockRejectionHook) HandleBidderRequestHook( result.NbrCode = m.nbr } - return result, nil + return result, m.err } func (m mockRejectionHook) HandleRawBidderResponseHook( diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 829b1f887b4..5f848446333 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -21,6 +21,7 @@ import ( "github.com/prebid/prebid-server/hooks" "github.com/prebid/prebid-server/hooks/hookexecution" "github.com/prebid/prebid-server/ortb" + "github.com/prebid/prebid-server/privacy" jsonpatch "gopkg.in/evanphx/json-patch.v4" accountService "github.com/prebid/prebid-server/account" @@ -56,6 +57,7 @@ func NewVideoEndpoint( defReqJSON []byte, bidderMap map[string]openrtb_ext.BidderName, cache prebid_cache_client.Client, + tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, ) (httprouter.Handle, error) { if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { @@ -89,7 +91,8 @@ func NewVideoEndpoint( videoEndpointRegexp, ipValidator, empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}}).VideoAuctionEndpoint), nil + hooks.EmptyPlanBuilder{}, + tmaxAdjustments}).VideoAuctionEndpoint), nil } /* @@ -275,7 +278,11 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re defer cancel() } - usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie)) + // Read Usersyncs/Cookie + decoder := usersync.Base64Decoder{} + usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie) + usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie) + if bidReqWrapper.App != nil { labels.Source = metrics.DemandApp labels.PubID = getAccountID(bidReqWrapper.App.Publisher) @@ -296,8 +303,17 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } + activities, activitiesErr := privacy.NewActivityControl(&account.Privacy) + if activitiesErr != nil { + errL = append(errL, activitiesErr) + if errortypes.ContainsFatalError(errL) { + handleError(&labels, w, errL, &vo, &debugLog) + return + } + } + secGPC := r.Header.Get("Sec-GPC") - auctionRequest := exchange.AuctionRequest{ + auctionRequest := &exchange.AuctionRequest{ BidRequestWrapper: bidReqWrapper, Account: *account, UserSyncs: usersyncs, @@ -307,11 +323,23 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re GlobalPrivacyControlHeader: secGPC, PubID: labels.PubID, HookExecutor: hookexecution.EmptyHookExecutor{}, + TmaxAdjustments: deps.tmaxAdjustments, + Activities: activities, } - response, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog) - vo.Request = bidReqWrapper.BidRequest + auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog) + defer func() { + if !auctionRequest.BidderResponseStartTime.IsZero() { + deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime)) + } + }() + vo.RequestWrapper = bidReqWrapper + var response *openrtb2.BidResponse + if auctionResponse != nil { + response = auctionResponse.BidResponse + } vo.Response = response + vo.SeatNonBid = auctionResponse.GetSeatNonBid() if err != nil { errL := []error{err} handleError(&labels, w, errL, &vo, &debugLog) @@ -326,6 +354,10 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } if bidReq.Test == 1 { + err = setSeatNonBidRaw(bidReqWrapper, auctionResponse) + if err != nil { + glog.Errorf("Error setting seat non-bid: %v", err) + } bidResp.Ext = response.Ext } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 4ad69e53ec5..3c2528e2aeb 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -65,6 +65,20 @@ func TestVideoEndpointImpressionsNumber(t *testing.T) { assert.Equal(t, resp.AdPods[0].Targeting[0].HbDeal, "ABC_123", "If DealID exists in bid response, hb_deal targeting needs to be added to resp") } +func TestVideoEndpointInvalidPrivacyConfig(t *testing.T) { + ex := &mockExchangeVideo{} + reqBody := readVideoTestFile(t, "sample-requests/video/video_valid_sample.json") + req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDepsInvalidPrivacy(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + respBytes := recorder.Body.Bytes() + expectedErrorMessage := "Critical error while running the video endpoint: unable to parse component: bidderA.BidderB.bidderC" + assert.Equal(t, expectedErrorMessage, string(respBytes), "error message is incorrect") +} + func TestVideoEndpointImpressionsDuration(t *testing.T) { ex := &mockExchangeVideo{} reqBody := readVideoTestFile(t, "sample-requests/video/video_valid_sample_different_durations.json") @@ -153,7 +167,7 @@ func TestCreateBidExtensionTargeting(t *testing.T) { require.NotNil(t, ex.lastRequest, "The request never made it into the Exchange.") // assert targeting set to default - expectedRequestExt := `{"prebid":{"cache":{"vastxml":{}},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includebidderkeys":true,"includewinners":true,"includebrandcategory":{"primaryadserver":1,"publisher":"","withcategory":true}}}}` + expectedRequestExt := `{"prebid":{"cache":{"vastxml":{}},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includebidderkeys":true,"includewinners":true,"includebrandcategory":{"primaryadserver":1,"publisher":"","withcategory":true}}}}` assert.JSONEq(t, expectedRequestExt, string(ex.lastRequest.Ext)) } @@ -1225,6 +1239,7 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *m hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } return deps, metrics, mockModule } @@ -1270,6 +1285,41 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, + } +} + +func mockDepsInvalidPrivacy(t *testing.T, ex *mockExchangeVideo) *endpointDeps { + return &endpointDeps{ + fakeUUIDGenerator{}, + ex, + mockBidderParamValidator{}, + &mockVideoStoredReqFetcher{}, + &mockVideoStoredReqFetcher{}, + &mockAccountFetcher{data: mockVideoAccountData}, + &config.Configuration{MaxRequestSize: maxSize, + AccountDefaults: config.Account{ + Privacy: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitPreciseGeo: config.Activity{Rules: []config.ActivityRule{ + {Condition: config.ActivityCondition{ComponentName: []string{"bidderA.BidderB.bidderC"}}}, + }}, + }, + }, + }, + }, + &metricsConfig.NilMetricsEngine{}, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + ex.cache, + regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, } } @@ -1293,6 +1343,7 @@ func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames) hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } return deps @@ -1318,6 +1369,7 @@ func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps { hardcodedResponseIPValidator{response: true}, empty_fetcher.EmptyFetcher{}, hooks.EmptyPlanBuilder{}, + nil, } return edep @@ -1354,7 +1406,7 @@ type mockExchangeVideo struct { cache *mockCacheClient } -func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { if err := r.BidRequestWrapper.RebuildRequest(); err != nil { return nil, err } @@ -1364,7 +1416,7 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionR m.cache.called = true } ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s","hb_size":"1x1", "hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1", "hb_deal_appnexus": "ABC_123"},"type":"video","dealpriority":0,"dealtiersatisfied":false},"bidder":{"appnexus":{"brand_id":1,"auction_id":7840037870526938650,"bidder_id":2,"bid_ad_type":1,"creative_info":{"video":{"duration":30,"mimes":["video\/mp4"]}}}}}`) - return &openrtb2.BidResponse{ + return &exchange.AuctionResponse{BidResponse: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{{ Seat: "appnexus", Bid: []openrtb2.Bid{ @@ -1386,7 +1438,7 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionR {ID: "16", ImpID: "5_2", Ext: ext}, }, }}, - }, nil + }}, nil } type mockExchangeAppendBidderNames struct { @@ -1394,13 +1446,13 @@ type mockExchangeAppendBidderNames struct { cache *mockCacheClient } -func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { m.lastRequest = r.BidRequestWrapper.BidRequest if debugLog != nil && debugLog.Enabled { m.cache.called = true } ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s_appnexus","hb_size":"1x1", "hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"},"type":"video"},"bidder":{"appnexus":{"brand_id":1,"auction_id":7840037870526938650,"bidder_id":2,"bid_ad_type":1,"creative_info":{"video":{"duration":30,"mimes":["video\/mp4"]}}}}}`) - return &openrtb2.BidResponse{ + return &exchange.AuctionResponse{BidResponse: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{{ Seat: "appnexus", Bid: []openrtb2.Bid{ @@ -1421,7 +1473,7 @@ func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r excha {ID: "15", ImpID: "5_1", Ext: ext}, {ID: "16", ImpID: "5_2", Ext: ext}, }, - }}, + }}}, }, nil } @@ -1430,11 +1482,11 @@ type mockExchangeVideoNoBids struct { cache *mockCacheClient } -func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { +func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { m.lastRequest = r.BidRequestWrapper.BidRequest - return &openrtb2.BidResponse{ + return &exchange.AuctionResponse{BidResponse: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{{}}, - }, nil + }}, nil } var mockVideoAccountData = map[string]json.RawMessage{ diff --git a/endpoints/setuid.go b/endpoints/setuid.go index a4d04749eae..30a1cce2751 100644 --- a/endpoints/setuid.go +++ b/endpoints/setuid.go @@ -3,21 +3,27 @@ package endpoints import ( "context" "errors" + "fmt" "net/http" "net/url" "strconv" "strings" - "time" "github.com/julienschmidt/httprouter" + gpplib "github.com/prebid/go-gpp" + gppConstants "github.com/prebid/go-gpp/constants" accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" + "github.com/prebid/prebid-server/privacy" + gppPrivacy "github.com/prebid/prebid-server/privacy/gpp" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/usersync" "github.com/prebid/prebid-server/util/httputil" + stringutil "github.com/prebid/prebid-server/util/stringutil" ) const ( @@ -28,15 +34,11 @@ const ( chromeiOSStrLen = len(chromeiOSStr) ) -func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, pbsanalytics analytics.PBSAnalyticsModule, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle { - cookieTTL := time.Duration(cfg.HostCookie.TTL) * 24 * time.Hour +const uidCookieName = "uids" - // convert map of syncers by bidder to map of syncers by key - // - its safe to assume that if multiple bidders map to the same key, the syncers are interchangeable. - syncersByKey := make(map[string]usersync.Syncer, len(syncersByBidder)) - for _, v := range syncersByBidder { - syncersByKey[v.Key()] = v - } +func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, pbsanalytics analytics.PBSAnalyticsModule, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle { + encoder := usersync.Base64Encoder{} + decoder := usersync.Base64Decoder{} return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { so := analytics.SetUIDObject{ @@ -46,17 +48,18 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use defer pbsanalytics.LogSetUIDObject(&so) - pc := usersync.ParseCookieFromRequest(r, &cfg.HostCookie) - if !pc.AllowSyncs() { + cookie := usersync.ReadCookie(r, decoder, &cfg.HostCookie) + if !cookie.AllowSyncs() { w.WriteHeader(http.StatusUnauthorized) metricsEngine.RecordSetUid(metrics.SetUidOptOut) so.Status = http.StatusUnauthorized return } + usersync.SyncHostCookie(r, cookie, &cfg.HostCookie) query := r.URL.Query() - syncer, err := getSyncer(query, syncersByKey) + syncer, bidderName, err := getSyncer(query, syncersByBidder) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) @@ -101,9 +104,37 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use return } + activities, activitiesErr := privacy.NewActivityControl(&account.Privacy) + if activitiesErr != nil { + if errortypes.ContainsFatalError([]error{activitiesErr}) { + activities = privacy.ActivityControl{} + } + } + + userSyncActivityAllowed := activities.Allow(privacy.ActivitySyncUser, + privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderName}) + if !userSyncActivityAllowed { + w.WriteHeader(http.StatusUnavailableForLegalReasons) + return + } + + gdprRequestInfo, err := extractGDPRInfo(query) + if err != nil { + // Only exit if non-warning + if !errortypes.IsWarning(err) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + metricsEngine.RecordSetUid(metrics.SetUidBadRequest) + so.Errors = []error{err} + so.Status = http.StatusBadRequest + return + } + w.Write([]byte("Warning: " + err.Error())) + } + tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR) - if shouldReturn, status, body := preventSyncsGDPR(query.Get("gdpr"), query.Get("gdpr_consent"), gdprPermsBuilder, tcf2Cfg); shouldReturn { + if shouldReturn, status, body := preventSyncsGDPR(gdprRequestInfo, gdprPermsBuilder, tcf2Cfg); shouldReturn { w.WriteHeader(status) w.Write([]byte(body)) switch status { @@ -121,18 +152,28 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use so.UID = uid if uid == "" { - pc.Unsync(syncer.Key()) + cookie.Unsync(syncer.Key()) metricsEngine.RecordSetUid(metrics.SetUidOK) metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidCleared) so.Success = true - } else if err = pc.TrySync(syncer.Key(), uid); err == nil { + } else if err = cookie.Sync(syncer.Key(), uid); err == nil { metricsEngine.RecordSetUid(metrics.SetUidOK) metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidOK) so.Success = true } setSiteCookie := siteCookieCheck(r.UserAgent()) - pc.SetCookieOnResponse(w, setSiteCookie, &cfg.HostCookie, cookieTTL) + + // Write Cookie + encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + metricsEngine.RecordSetUid(metrics.SetUidBadRequest) + so.Errors = []error{err} + so.Status = http.StatusBadRequest + return + } + usersync.WriteCookie(w, encodedCookie, &cfg.HostCookie, setSiteCookie) switch responseFormat { case "i": @@ -148,19 +189,150 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use }) } -func getSyncer(query url.Values, syncersByKey map[string]usersync.Syncer) (usersync.Syncer, error) { - key := query.Get("bidder") +// extractGDPRInfo looks for the GDPR consent string and GDPR signal in the GPP query params +// first and the 'gdpr' and 'gdpr_consent' query params second. If found in both, throws a +// warning. Can also throw a parsing or validation error +func extractGDPRInfo(query url.Values) (reqInfo gdpr.RequestInfo, err error) { + reqInfo, err = parseGDPRFromGPP(query) + if err != nil { + return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err + } + + legacySignal, legacyConsent, err := parseLegacyGDPRFields(query, reqInfo.GDPRSignal, reqInfo.Consent) + isWarning := errortypes.IsWarning(err) + + if err != nil && !isWarning { + return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err + } + + // If no GDPR data in the GPP fields, use legacy instead + if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalAmbiguous { + reqInfo.GDPRSignal = legacySignal + reqInfo.Consent = legacyConsent + } + + if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalYes { + return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, errors.New("GDPR consent is required when gdpr signal equals 1") + } + + return reqInfo, err +} + +// parseGDPRFromGPP parses and validates the "gpp_sid" and "gpp" query fields. +func parseGDPRFromGPP(query url.Values) (gdpr.RequestInfo, error) { + var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous + var gdprConsent string = "" + var err error + + gdprSignal, err = parseSignalFromGppSidStr(query.Get("gpp_sid")) + if err != nil { + return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err + } + + gdprConsent, err = parseConsentFromGppStr(query.Get("gpp")) + if err != nil { + return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err + } + + return gdpr.RequestInfo{ + Consent: gdprConsent, + GDPRSignal: gdprSignal, + }, nil +} + +// parseLegacyGDPRFields parses and validates the "gdpr" and "gdpr_consent" query fields which +// are considered deprecated in favor of the "gpp" and "gpp_sid". The parsed and validated GDPR +// values contained in "gpp" and "gpp_sid" are passed in the parameters gppGDPRSignal and +// gppGDPRConsent. If the GPP parameters come with non-default values, this function discards +// "gdpr" and "gdpr_consent" and returns a warning. +func parseLegacyGDPRFields(query url.Values, gppGDPRSignal gdpr.Signal, gppGDPRConsent string) (gdpr.Signal, string, error) { + var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous + var gdprConsent string + var warning error + + if gdprQuerySignal := query.Get("gdpr"); len(gdprQuerySignal) > 0 { + if gppGDPRSignal == gdpr.SignalAmbiguous { + switch gdprQuerySignal { + case "0": + fallthrough + case "1": + if zeroOrOne, err := strconv.Atoi(gdprQuerySignal); err == nil { + gdprSignal = gdpr.Signal(zeroOrOne) + } + default: + return gdpr.SignalAmbiguous, "", errors.New("the gdpr query param must be either 0 or 1. You gave " + gdprQuerySignal) + } + } else { + warning = &errortypes.Warning{ + Message: "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.", + WarningCode: errortypes.UnknownWarningCode, + } + } + } + + if gdprLegacyConsent := query.Get("gdpr_consent"); len(gdprLegacyConsent) > 0 { + if len(gppGDPRConsent) > 0 { + warning = &errortypes.Warning{ + Message: "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.", + WarningCode: errortypes.UnknownWarningCode, + } + } else { + gdprConsent = gdprLegacyConsent + } + } + return gdprSignal, gdprConsent, warning +} + +func parseSignalFromGppSidStr(strSID string) (gdpr.Signal, error) { + gdprSignal := gdpr.SignalAmbiguous + + if len(strSID) > 0 { + gppSID, err := stringutil.StrToInt8Slice(strSID) + if err != nil { + return gdpr.SignalAmbiguous, fmt.Errorf("Error parsing gpp_sid %s", err.Error()) + } + + if len(gppSID) > 0 { + gdprSignal = gdpr.SignalNo + if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) { + gdprSignal = gdpr.SignalYes + } + } + } + + return gdprSignal, nil +} + +func parseConsentFromGppStr(gppQueryValue string) (string, error) { + var gdprConsent string + + if len(gppQueryValue) > 0 { + gpp, err := gpplib.Parse(gppQueryValue) + if err != nil { + return "", err + } - if key == "" { - return nil, errors.New(`"bidder" query param is required`) + if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 { + gdprConsent = gpp.Sections[i].GetValue() + } } - syncer, syncerExists := syncersByKey[key] + return gdprConsent, nil +} + +func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, string, error) { + bidder := query.Get("bidder") + + if bidder == "" { + return nil, "", errors.New(`"bidder" query param is required`) + } + + syncer, syncerExists := syncersByBidder[bidder] if !syncerExists { - return nil, errors.New("The bidder name provided is not supported by Prebid Server") + return nil, "", errors.New("The bidder name provided is not supported by Prebid Server") } - return syncer, nil + return syncer, bidder, nil } // getResponseFormat reads the format query parameter or falls back to the syncer's default. @@ -216,26 +388,7 @@ func checkChromeBrowserVersion(ua string, index int, chromeStrLength int) bool { return result } -func preventSyncsGDPR(gdprEnabled string, gdprConsent string, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) { - if gdprEnabled != "" && gdprEnabled != "0" && gdprEnabled != "1" { - return true, http.StatusBadRequest, "the gdpr query param must be either 0 or 1. You gave " + gdprEnabled - } - - if gdprEnabled == "1" && gdprConsent == "" { - return true, http.StatusBadRequest, "gdpr_consent is required when gdpr=1" - } - - gdprSignal := gdpr.SignalAmbiguous - - if i, err := strconv.Atoi(gdprEnabled); err == nil { - gdprSignal = gdpr.Signal(i) - } - - gdprRequestInfo := gdpr.RequestInfo{ - Consent: gdprConsent, - GDPRSignal: gdprSignal, - } - +func preventSyncsGDPR(gdprRequestInfo gdpr.RequestInfo, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) { perms := permsBuilder(tcf2Cfg, gdprRequestInfo) allowed, err := perms.HostCookiesAllowed(context.Background()) diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 609d85395fd..05030cc7cf7 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -13,6 +13,7 @@ import ( "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" @@ -49,7 +50,7 @@ func TestSetUIDEndpoint(t *testing.T) { description: "Set uid for valid bidder", }, { - uri: "/setuid?bidder=adnxs&uid=123", + uri: "/setuid?bidder=appnexus&uid=123", syncersBidderNameToKey: map[string]string{"appnexus": "adnxs"}, existingSyncs: nil, gdprAllowsHostCookies: true, @@ -157,7 +158,7 @@ func TestSetUIDEndpoint(t *testing.T) { expectedSyncs: nil, gdprAllowsHostCookies: true, expectedStatusCode: http.StatusBadRequest, - expectedBody: "gdpr_consent is required when gdpr=1", + expectedBody: "GDPR consent is required when gdpr signal equals 1", description: "Return an error if GDPR is set to 1 but GDPR consent string is missing", }, { @@ -193,6 +194,28 @@ func TestSetUIDEndpoint(t *testing.T) { expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"}, description: "Should set uid for a bidder that is allowed by the GDPR consent string", }, + { + uri: "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + gdprAllowsHostCookies: true, + existingSyncs: nil, + expectedSyncs: map[string]string{"pubmatic": "123"}, + expectedStatusCode: http.StatusOK, + expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"}, + description: "Sets uid for a bidder allowed by GDPR consent string in the GPP query field", + }, + { + uri: "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA" + + "gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + gdprAllowsHostCookies: true, + existingSyncs: nil, + expectedSyncs: map[string]string{"pubmatic": "123"}, + expectedStatusCode: http.StatusOK, + expectedBody: "Warning: 'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.", + expectedHeaders: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, + description: "Sets uid for a bidder allowed by GDPR in GPP, throws warning because GDPR legacy values weren't used", + }, { uri: "/setuid?bidder=pubmatic&uid=123&gdpr=1&gdpr_consent=" + "malformed", @@ -254,6 +277,35 @@ func TestSetUIDEndpoint(t *testing.T) { expectedBody: "account is disabled, please reach out to the prebid server host", description: "Set uid for valid bidder with valid disabled account provided", }, + { + uri: "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_valid_activities_usersync_enabled", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + existingSyncs: nil, + gdprAllowsHostCookies: true, + expectedSyncs: map[string]string{"pubmatic": "123"}, + expectedStatusCode: http.StatusOK, + expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"}, + description: "Set uid for valid bidder with valid account provided with user sync allowed activity", + }, + { + uri: "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_valid_activities_usersync_disabled", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + existingSyncs: nil, + gdprAllowsHostCookies: true, + expectedSyncs: nil, + expectedStatusCode: http.StatusUnavailableForLegalReasons, + description: "Set uid for valid bidder with valid account provided with user sync disallowed activity", + }, + { + uri: "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_invalid_activities", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + existingSyncs: nil, + gdprAllowsHostCookies: true, + expectedSyncs: map[string]string{"pubmatic": "123"}, + expectedStatusCode: http.StatusOK, + expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"}, + description: "Set uid for valid bidder with valid account provided with invalid user sync activity", + }, } analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{}) @@ -288,6 +340,597 @@ func TestSetUIDEndpoint(t *testing.T) { } } +func TestParseSignalFromGPPSID(t *testing.T) { + type testOutput struct { + signal gdpr.Signal + err error + } + testCases := []struct { + desc string + strSID string + expected testOutput + }{ + { + desc: "Empty gpp_sid, expect gdpr.SignalAmbiguous", + strSID: "", + expected: testOutput{ + signal: gdpr.SignalAmbiguous, + err: nil, + }, + }, + { + desc: "Malformed gpp_sid, expect gdpr.SignalAmbiguous", + strSID: "malformed", + expected: testOutput{ + signal: gdpr.SignalAmbiguous, + err: errors.New(`Error parsing gpp_sid strconv.ParseInt: parsing "malformed": invalid syntax`), + }, + }, + { + desc: "Valid gpp_sid doesn't come with TCF2, expect gdpr.SignalNo", + strSID: "6", + expected: testOutput{ + signal: gdpr.SignalNo, + err: nil, + }, + }, + { + desc: "Valid gpp_sid comes with TCF2, expect gdpr.SignalYes", + strSID: "2", + expected: testOutput{ + signal: gdpr.SignalYes, + err: nil, + }, + }, + } + for _, tc := range testCases { + outSignal, outErr := parseSignalFromGppSidStr(tc.strSID) + + assert.Equal(t, tc.expected.signal, outSignal, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + } +} + +func TestParseConsentFromGppStr(t *testing.T) { + type testOutput struct { + gdprConsent string + err error + } + testCases := []struct { + desc string + inGppQuery string + expected testOutput + }{ + { + desc: "Empty gpp field, expect empty GDPR consent", + inGppQuery: "", + expected: testOutput{ + gdprConsent: "", + err: nil, + }, + }, + { + desc: "Malformed gpp field value, expect empty GDPR consent and error", + inGppQuery: "malformed", + expected: testOutput{ + gdprConsent: "", + err: errors.New(`error parsing GPP header, base64 decoding: illegal base64 data at input byte 8`), + }, + }, + { + desc: "Valid gpp string comes with TCF2 in its gppConstants.SectionID's, expect non-empty GDPR consent", + inGppQuery: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + expected: testOutput{ + gdprConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + err: nil, + }, + }, + { + desc: "Valid gpp string doesn't come with TCF2 in its gppConstants.SectionID's, expect blank GDPR consent", + inGppQuery: "DBABjw~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN", + expected: testOutput{ + gdprConsent: "", + err: nil, + }, + }, + } + for _, tc := range testCases { + outConsent, outErr := parseConsentFromGppStr(tc.inGppQuery) + + assert.Equal(t, tc.expected.gdprConsent, outConsent, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + } +} + +func TestParseGDPRFromGPP(t *testing.T) { + type testOutput struct { + reqInfo gdpr.RequestInfo + err error + } + type aTest struct { + desc string + inUri string + expected testOutput + } + testGroups := []struct { + groupDesc string + testCases []aTest + }{ + { + groupDesc: "No gpp_sid nor gpp", + testCases: []aTest{ + { + desc: "Input URL is mising gpp_sid and gpp, expect signal ambiguous and no error", + inUri: "/setuid?bidder=pubmatic&uid=123", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: nil, + }, + }, + }, + }, + { + groupDesc: "gpp only", + testCases: []aTest{ + { + desc: "gpp is malformed, expect error", + inUri: "/setuid?gpp=malformed", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("error parsing GPP header, base64 decoding: illegal base64 data at input byte 8"), + }, + }, + { + desc: "gpp with a valid TCF2 value. Expect valid consent string and no error", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{ + GDPRSignal: gdpr.SignalAmbiguous, + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + }, + err: nil, + }, + }, + { + desc: "gpp does not include TCF2 string. Expect empty consent string and no error", + inUri: "/setuid?gpp=DBABjw~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{ + GDPRSignal: gdpr.SignalAmbiguous, + Consent: "", + }, + err: nil, + }, + }, + }, + }, + { + groupDesc: "gpp_sid only", + testCases: []aTest{ + { + desc: "gpp_sid is malformed, expect error", + inUri: "/setuid?gpp_sid=malformed", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("Error parsing gpp_sid strconv.ParseInt: parsing \"malformed\": invalid syntax"), + }, + }, + { + desc: "TCF2 found in gpp_sid list. Given that the consent string will be empty, expect an error", + inUri: "/setuid?gpp_sid=2,6", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalYes}, + err: nil, + }, + }, + { + desc: "TCF2 not found in gpp_sid list. Expect SignalNo and no error", + inUri: "/setuid?gpp_sid=6,8", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalNo}, + err: nil, + }, + }, + }, + }, + { + groupDesc: "both gpp_sid and gpp", + testCases: []aTest{ + { + desc: "TCF2 found in gpp_sid list and gpp has a valid GDPR string. Expect no error", + inUri: "/setuid?gpp_sid=2,6&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + expected: testOutput{ + reqInfo: gdpr.RequestInfo{ + GDPRSignal: gdpr.SignalYes, + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + }, + err: nil, + }, + }, + }, + }, + } + for _, tgroup := range testGroups { + for _, tc := range tgroup.testCases { + // set test + testURL, err := url.Parse(tc.inUri) + assert.NoError(t, err, "%s - %s", tgroup.groupDesc, tc.desc) + + query := testURL.Query() + + // run + outReqInfo, outErr := parseGDPRFromGPP(query) + + // assertions + assert.Equal(t, tc.expected.reqInfo, outReqInfo, "%s - %s", tgroup.groupDesc, tc.desc) + assert.Equal(t, tc.expected.err, outErr, "%s - %s", tgroup.groupDesc, tc.desc) + } + } +} + +func TestParseLegacyGDPRFields(t *testing.T) { + type testInput struct { + uri string + gppGDPRSignal gdpr.Signal + gppGDPRConsent string + } + type testOutput struct { + signal gdpr.Signal + consent string + err error + } + testCases := []struct { + desc string + in testInput + expected testOutput + }{ + { + desc: `both "gdpr" and "gdpr_consent" missing from URI, expect SignalAmbiguous, blank consent and no error`, + in: testInput{ + uri: "/setuid?bidder=pubmatic&uid=123", + }, + expected: testOutput{ + signal: gdpr.SignalAmbiguous, + consent: "", + err: nil, + }, + }, + { + desc: `invalid "gdpr" value, expect SignalAmbiguous, blank consent and error`, + in: testInput{ + uri: "/setuid?gdpr=2", + gppGDPRSignal: gdpr.SignalAmbiguous, + }, + expected: testOutput{ + signal: gdpr.SignalAmbiguous, + consent: "", + err: errors.New("the gdpr query param must be either 0 or 1. You gave 2"), + }, + }, + { + desc: `valid "gdpr" value but valid GDPRSignal was previously parsed before, expect SignalAmbiguous, blank consent and a warning`, + in: testInput{ + uri: "/setuid?gdpr=1", + gppGDPRSignal: gdpr.SignalYes, + }, + expected: testOutput{ + signal: gdpr.SignalAmbiguous, + consent: "", + err: &errortypes.Warning{ + Message: "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.", + WarningCode: errortypes.UnknownWarningCode, + }, + }, + }, + { + desc: `valid "gdpr_consent" value but valid GDPRSignal was previously parsed before, expect SignalAmbiguous, blank consent and a warning`, + in: testInput{ + uri: "/setuid?gdpr_consent=someConsent", + gppGDPRConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + }, + expected: testOutput{ + signal: gdpr.SignalAmbiguous, + consent: "", + err: &errortypes.Warning{ + Message: "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.", + WarningCode: errortypes.UnknownWarningCode, + }, + }, + }, + } + for _, tc := range testCases { + // set test + testURL, err := url.Parse(tc.in.uri) + assert.NoError(t, err, tc.desc) + + query := testURL.Query() + + // run + outSignal, outConsent, outErr := parseLegacyGDPRFields(query, tc.in.gppGDPRSignal, tc.in.gppGDPRConsent) + + // assertions + assert.Equal(t, tc.expected.signal, outSignal, tc.desc) + assert.Equal(t, tc.expected.consent, outConsent, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + } +} + +func TestExtractGDPRInfo(t *testing.T) { + type testOutput struct { + requestInfo gdpr.RequestInfo + err error + } + type testCase struct { + desc string + inUri string + expected testOutput + } + testSuite := []struct { + sDesc string + tests []testCase + }{ + { + sDesc: "no gdpr nor gpp values in query", + tests: []testCase{ + { + desc: "expect blank consent, signalNo and nil error", + inUri: "/setuid?bidder=pubmatic&uid=123", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "", + GDPRSignal: gdpr.SignalAmbiguous, + }, + err: nil, + }, + }, + }, + }, + { + sDesc: "missing gpp, gdpr only", + tests: []testCase{ + { + desc: "Invalid gdpr signal value in query, expect blank request info and error", + inUri: "/setuid?gdpr=2", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("the gdpr query param must be either 0 or 1. You gave 2"), + }, + }, + { + desc: "GDPR equals 0, blank consent, expect blank consent, signalNo and nil error", + inUri: "/setuid?gdpr=0", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalNo}, + err: nil, + }, + }, + { + desc: "GDPR equals 1, blank consent, expect blank request info and error", + inUri: "/setuid?gdpr=1", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("GDPR consent is required when gdpr signal equals 1"), + }, + }, + { + desc: "GDPR equals 0, non-blank consent, expect non-blank request info and nil error", + inUri: "/setuid?gdpr=0&gdpr_consent=someConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "someConsent", + GDPRSignal: gdpr.SignalNo, + }, + err: nil, + }, + }, + { + desc: "GDPR equals 1, non-blank consent, expect non-blank request info and nil error", + inUri: "/setuid?gdpr=1&gdpr_consent=someConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "someConsent", + GDPRSignal: gdpr.SignalYes, + }, + err: nil, + }, + }, + }, + }, + { + sDesc: "missing gdpr, gpp only", + tests: []testCase{ + { + desc: "Malformed GPP_SID string, expect blank request info and error", + inUri: "/setuid?gpp_sid=malformed", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("Error parsing gpp_sid strconv.ParseInt: parsing \"malformed\": invalid syntax"), + }, + }, + { + desc: "Valid GPP_SID string but invalid GPP string in query, expect blank request info and error", + inUri: "/setuid?gpp=malformed&gpp_sid=2", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("error parsing GPP header, base64 decoding: illegal base64 data at input byte 8"), + }, + }, + { + desc: "SectionTCFEU2 not found in GPP string, expect blank consent and signalAmbiguous", + inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "", + GDPRSignal: gdpr.SignalAmbiguous, + }, + err: nil, + }, + }, + { + desc: "No GPP string, nor SectionTCFEU2 found in SID list in query, expect blank consent and signalAmbiguous", + inUri: "/setuid?gpp_sid=3,6", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "", + GDPRSignal: gdpr.SignalNo, + }, + err: nil, + }, + }, + { + desc: "No GPP string, SectionTCFEU2 found in SID list in query, expect blank request info and error", + inUri: "/setuid?gpp_sid=2", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("GDPR consent is required when gdpr signal equals 1"), + }, + }, + { + desc: "SectionTCFEU2 only found in SID list, expect blank request info and error", + inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=2", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, + err: errors.New("GDPR consent is required when gdpr signal equals 1"), + }, + }, + { + desc: "SectionTCFEU2 found in GPP string but SID list is nil, expect valid consent and SignalAmbiguous", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + GDPRSignal: gdpr.SignalAmbiguous, + }, + err: nil, + }, + }, + { + desc: "SectionTCFEU2 found in GPP string but not in the non-nil SID list, expect valid consent and signalNo", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=6", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + GDPRSignal: gdpr.SignalNo, + }, + err: nil, + }, + }, + { + desc: "SectionTCFEU2 found both in GPP string and SID list, expect valid consent and signalYes", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + GDPRSignal: gdpr.SignalYes, + }, + err: nil, + }, + }, + }, + }, + { + sDesc: "GPP values take priority over GDPR", + tests: []testCase{ + { + desc: "SignalNo in gdpr field but SignalYes in SID list, CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA consent in gpp but legacyConsent in gdpr_consent, expect GPP values to prevail", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4&gdpr=0&gdpr_consent=legacyConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + GDPRSignal: gdpr.SignalYes, + }, + err: &errortypes.Warning{ + Message: "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.", + WarningCode: errortypes.UnknownWarningCode, + }, + }, + }, + { + desc: "SignalNo in gdpr field but SignalYes in SID list because SectionTCFEU2 is listed, expect GPP to prevail", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4&gdpr=0", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + GDPRSignal: gdpr.SignalYes, + }, + err: &errortypes.Warning{ + Message: "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.", + WarningCode: errortypes.UnknownWarningCode, + }, + }, + }, + { + desc: "No gpp string in URL query, use gdpr_consent and SignalYes found in SID list because SectionTCFEU2 is listed", + inUri: "/setuid?gpp_sid=2,4&gdpr_consent=legacyConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "", + GDPRSignal: gdpr.SignalAmbiguous, + }, + err: errors.New("GDPR consent is required when gdpr signal equals 1"), + }, + }, + { + desc: "SectionTCFEU2 not found in GPP string but found in SID list, choose the GDPR_CONSENT and GPP_SID signal", + inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=2&gdpr=0&gdpr_consent=legacyConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "", + GDPRSignal: gdpr.SignalAmbiguous, + }, + err: errors.New("GDPR consent is required when gdpr signal equals 1"), + }, + }, + { + desc: "SectionTCFEU2 found in GPP string but not in SID list, choose GDPR signal GPP consent value", + inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=6&gdpr=1&gdpr_consent=legacyConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", + GDPRSignal: gdpr.SignalNo, + }, + err: &errortypes.Warning{ + Message: "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.", + WarningCode: errortypes.UnknownWarningCode, + }, + }, + }, + { + desc: "SectionTCFEU2 not found in GPP, use GDPR_CONSENT value. SignalYes found in gdpr field, but not in the valid SID list, expect SignalNo", + inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=6&gdpr=1&gdpr_consent=legacyConsent", + expected: testOutput{ + requestInfo: gdpr.RequestInfo{ + Consent: "", + GDPRSignal: gdpr.SignalNo, + }, + err: &errortypes.Warning{ + Message: "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.", + WarningCode: errortypes.UnknownWarningCode, + }, + }, + }, + }, + }, + } + + for _, ts := range testSuite { + for _, tc := range ts.tests { + // set test + testURL, err := url.Parse(tc.inUri) + assert.NoError(t, err, tc.desc) + + query := testURL.Query() + + // run + outReqInfo, outErr := extractGDPRInfo(query) + + // assertions + assert.Equal(t, tc.expected.requestInfo, outReqInfo, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + } + } +} + func TestSetUIDEndpointMetrics(t *testing.T) { cookieWithOptOut := usersync.NewCookie() cookieWithOptOut.SetOptOut(true) @@ -425,7 +1068,7 @@ func TestSetUIDEndpointMetrics(t *testing.T) { Status: 400, Bidder: "pubmatic", UID: "", - Errors: []error{errors.New("gdpr_consent is required when gdpr=1")}, + Errors: []error{errors.New("GDPR consent is required when gdpr signal equals 1")}, Success: false, } a.On("LogSetUIDObject", &expected).Once() @@ -697,7 +1340,7 @@ func makeRequest(uri string, existingSyncs map[string]string) *http.Request { if len(existingSyncs) > 0 { pbsCookie := usersync.NewCookie() for key, value := range existingSyncs { - pbsCookie.TrySync(key, value) + pbsCookie.Sync(key, value) } addCookie(request, pbsCookie) } @@ -740,6 +1383,10 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric "disabled_acct": json.RawMessage(`{"disabled":true}`), "malformed_acct": json.RawMessage(`{"disabled":"malformed"}`), "invalid_json_acct": json.RawMessage(`{"}`), + + "valid_acct_with_valid_activities_usersync_enabled": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"default": true}}}}`), + "valid_acct_with_valid_activities_usersync_disabled": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"default": false}}}}`), + "valid_acct_with_invalid_activities": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`), }} endpoint := NewSetUIDEndpoint(&cfg, syncersByBidder, gdprPermsBuilder, tcf2ConfigBuilder, analytics, fakeAccountsFetcher, metrics) @@ -749,10 +1396,12 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric } func addCookie(req *http.Request, cookie *usersync.Cookie) { - req.AddCookie(cookie.ToHTTPCookie(time.Duration(1) * time.Hour)) + httpCookie, _ := ToHTTPCookie(cookie) + req.AddCookie(httpCookie) } func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *usersync.Cookie { + decoder := usersync.Base64Decoder{} cookieString := response.Header().Get("Set-Cookie") parser := regexp.MustCompile("uids=(.*?);") res := parser.FindStringSubmatch(cookieString) @@ -761,7 +1410,7 @@ func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *users Name: "uids", Value: res[1], } - return usersync.ParseCookie(&httpCookie) + return decoder.Decode(httpCookie.Value) } type fakePermissionsBuilder struct { @@ -830,3 +1479,18 @@ func (s fakeSyncer) SupportsType(syncTypes []usersync.SyncType) bool { func (s fakeSyncer) GetSync(syncTypes []usersync.SyncType, privacyPolicies privacy.Policies) (usersync.Sync, error) { return usersync.Sync{}, nil } + +func ToHTTPCookie(cookie *usersync.Cookie) (*http.Cookie, error) { + encoder := usersync.Base64Encoder{} + encodedCookie, err := encoder.Encode(cookie) + if err != nil { + return nil, nil + } + + return &http.Cookie{ + Name: uidCookieName, + Value: encodedCookie, + Expires: time.Now().Add((90 * 24 * time.Hour)), + Path: "/", + }, nil +} diff --git a/errortypes/code.go b/errortypes/code.go index f81ee464e76..c68eb19607a 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -26,6 +26,8 @@ const ( AlternateBidderCodeWarningCode MultiBidWarningCode AdServerTargetingWarningCode + BidAdjustmentWarningCode + FloorBidRejectionWarningCode ) // Coder provides an error or warning code with severity. diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index d93075b7c6c..aff0482280e 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -183,7 +183,8 @@ func (err *MalformedAcct) Severity() Severity { return SeverityFatal } -// Warning is a generic non-fatal error. +// Warning is a generic non-fatal error. Throughout the codebase, an error can +// only be a warning if it's of the type defined below type Warning struct { Message string WarningCode int diff --git a/errortypes/severity.go b/errortypes/severity.go index 0838b09592e..5f9cd80dd28 100644 --- a/errortypes/severity.go +++ b/errortypes/severity.go @@ -20,7 +20,10 @@ func isFatal(err error) bool { return !ok || s.Severity() == SeverityFatal } -func isWarning(err error) bool { +// IsWarning returns true if an error is labeled with a Severity of SeverityWarning +// Throughout the codebase, errors with SeverityWarning are of the type Warning +// defined in this package +func IsWarning(err error) bool { s, ok := err.(Coder) return ok && s.Severity() == SeverityWarning } @@ -54,7 +57,7 @@ func WarningOnly(errs []error) []error { errsWarning := make([]error, 0, len(errs)) for _, err := range errs { - if isWarning(err) { + if IsWarning(err) { errsWarning = append(errsWarning, err) } } diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 03889267202..a9ee85c307a 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -19,6 +19,7 @@ import ( "github.com/prebid/prebid-server/adapters/adot" "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adprime" + "github.com/prebid/prebid-server/adapters/adquery" "github.com/prebid/prebid-server/adapters/adrino" "github.com/prebid/prebid-server/adapters/adsinteractive" "github.com/prebid/prebid-server/adapters/adtarget" @@ -28,6 +29,7 @@ import ( "github.com/prebid/prebid-server/adapters/adview" "github.com/prebid/prebid-server/adapters/adxcg" "github.com/prebid/prebid-server/adapters/adyoulike" + "github.com/prebid/prebid-server/adapters/aidem" "github.com/prebid/prebid-server/adapters/aja" "github.com/prebid/prebid-server/adapters/algorix" "github.com/prebid/prebid-server/adapters/amx" @@ -38,9 +40,11 @@ import ( "github.com/prebid/prebid-server/adapters/audienceNetwork" "github.com/prebid/prebid-server/adapters/automatad" "github.com/prebid/prebid-server/adapters/avocet" + "github.com/prebid/prebid-server/adapters/axis" "github.com/prebid/prebid-server/adapters/axonix" "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/beintoo" + "github.com/prebid/prebid-server/adapters/bematterfull" "github.com/prebid/prebid-server/adapters/between" "github.com/prebid/prebid-server/adapters/beyondmedia" "github.com/prebid/prebid-server/adapters/bidmachine" @@ -50,10 +54,11 @@ import ( "github.com/prebid/prebid-server/adapters/bizzclick" "github.com/prebid/prebid-server/adapters/bliink" "github.com/prebid/prebid-server/adapters/blue" + "github.com/prebid/prebid-server/adapters/bluesea" "github.com/prebid/prebid-server/adapters/bmtm" "github.com/prebid/prebid-server/adapters/boldwin" "github.com/prebid/prebid-server/adapters/brave" - "github.com/prebid/prebid-server/adapters/brightroll" + cadentaperturemx "github.com/prebid/prebid-server/adapters/cadent_aperture_mx" "github.com/prebid/prebid-server/adapters/ccx" "github.com/prebid/prebid-server/adapters/coinzilla" "github.com/prebid/prebid-server/adapters/colossus" @@ -71,14 +76,17 @@ import ( "github.com/prebid/prebid-server/adapters/dianomi" "github.com/prebid/prebid-server/adapters/dmx" evolution "github.com/prebid/prebid-server/adapters/e_volution" - "github.com/prebid/prebid-server/adapters/emx_digital" + "github.com/prebid/prebid-server/adapters/emtv" "github.com/prebid/prebid-server/adapters/engagebdr" "github.com/prebid/prebid-server/adapters/eplanning" "github.com/prebid/prebid-server/adapters/epom" + "github.com/prebid/prebid-server/adapters/flipp" "github.com/prebid/prebid-server/adapters/freewheelssp" + "github.com/prebid/prebid-server/adapters/frvradn" "github.com/prebid/prebid-server/adapters/gamma" "github.com/prebid/prebid-server/adapters/gamoshi" "github.com/prebid/prebid-server/adapters/globalsun" + "github.com/prebid/prebid-server/adapters/gothamads" "github.com/prebid/prebid-server/adapters/grid" "github.com/prebid/prebid-server/adapters/gumgum" "github.com/prebid/prebid-server/adapters/huaweiads" @@ -98,7 +106,9 @@ import ( "github.com/prebid/prebid-server/adapters/kiviads" "github.com/prebid/prebid-server/adapters/krushmedia" "github.com/prebid/prebid-server/adapters/kubient" + "github.com/prebid/prebid-server/adapters/liftoff" "github.com/prebid/prebid-server/adapters/limelightDigital" + lmkiviads "github.com/prebid/prebid-server/adapters/lm_kiviads" "github.com/prebid/prebid-server/adapters/lockerdome" "github.com/prebid/prebid-server/adapters/logan" "github.com/prebid/prebid-server/adapters/logicad" @@ -108,8 +118,10 @@ import ( "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/medianet" "github.com/prebid/prebid-server/adapters/mgid" + "github.com/prebid/prebid-server/adapters/mgidX" "github.com/prebid/prebid-server/adapters/mobfoxpb" "github.com/prebid/prebid-server/adapters/mobilefuse" + "github.com/prebid/prebid-server/adapters/motorik" "github.com/prebid/prebid-server/adapters/nanointeractive" "github.com/prebid/prebid-server/adapters/nextmillennium" "github.com/prebid/prebid-server/adapters/ninthdecimal" @@ -120,6 +132,7 @@ import ( "github.com/prebid/prebid-server/adapters/operaads" "github.com/prebid/prebid-server/adapters/orbidder" "github.com/prebid/prebid-server/adapters/outbrain" + "github.com/prebid/prebid-server/adapters/ownadx" "github.com/prebid/prebid-server/adapters/pangle" "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pubnative" @@ -128,12 +141,15 @@ import ( "github.com/prebid/prebid-server/adapters/revcontent" "github.com/prebid/prebid-server/adapters/rhythmone" "github.com/prebid/prebid-server/adapters/richaudience" + "github.com/prebid/prebid-server/adapters/rise" "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" salunamedia "github.com/prebid/prebid-server/adapters/sa_lunamedia" + "github.com/prebid/prebid-server/adapters/screencore" "github.com/prebid/prebid-server/adapters/seedingAlliance" "github.com/prebid/prebid-server/adapters/sharethrough" "github.com/prebid/prebid-server/adapters/silvermob" + "github.com/prebid/prebid-server/adapters/silverpush" "github.com/prebid/prebid-server/adapters/smaato" "github.com/prebid/prebid-server/adapters/smartadserver" "github.com/prebid/prebid-server/adapters/smarthub" @@ -148,6 +164,7 @@ import ( "github.com/prebid/prebid-server/adapters/taboola" "github.com/prebid/prebid-server/adapters/tappx" "github.com/prebid/prebid-server/adapters/telaria" + "github.com/prebid/prebid-server/adapters/tpmn" "github.com/prebid/prebid-server/adapters/trafficgate" "github.com/prebid/prebid-server/adapters/triplelift" "github.com/prebid/prebid-server/adapters/triplelift_native" @@ -160,13 +177,16 @@ import ( "github.com/prebid/prebid-server/adapters/vidoomy" "github.com/prebid/prebid-server/adapters/visiblemeasures" "github.com/prebid/prebid-server/adapters/visx" + "github.com/prebid/prebid-server/adapters/vox" "github.com/prebid/prebid-server/adapters/vrtcal" - "github.com/prebid/prebid-server/adapters/yahoossp" + "github.com/prebid/prebid-server/adapters/xeworks" + "github.com/prebid/prebid-server/adapters/yahooAds" "github.com/prebid/prebid-server/adapters/yeahmobi" "github.com/prebid/prebid-server/adapters/yieldlab" "github.com/prebid/prebid-server/adapters/yieldmo" "github.com/prebid/prebid-server/adapters/yieldone" "github.com/prebid/prebid-server/adapters/zeroclickfraud" + "github.com/prebid/prebid-server/adapters/zeta_global_ssp" "github.com/prebid/prebid-server/openrtb_ext" ) @@ -193,8 +213,10 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdot: adot.Builder, openrtb_ext.BidderAdpone: adpone.Builder, openrtb_ext.BidderAdprime: adprime.Builder, + openrtb_ext.BidderAdquery: adquery.Builder, openrtb_ext.BidderAdrino: adrino.Builder, openrtb_ext.BidderAdsinteractive: adsinteractive.Builder, + openrtb_ext.BidderAdsyield: limelightDigital.Builder, openrtb_ext.BidderAdtarget: adtarget.Builder, openrtb_ext.BidderAdtrgtme: adtrgtme.Builder, openrtb_ext.BidderAdtelligent: adtelligent.Builder, @@ -202,6 +224,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdView: adview.Builder, openrtb_ext.BidderAdxcg: adxcg.Builder, openrtb_ext.BidderAdyoulike: adyoulike.Builder, + openrtb_ext.BidderAidem: aidem.Builder, openrtb_ext.BidderAJA: aja.Builder, openrtb_ext.BidderAlgorix: algorix.Builder, openrtb_ext.BidderAMX: amx.Builder, @@ -212,9 +235,11 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAudienceNetwork: audienceNetwork.Builder, openrtb_ext.BidderAutomatad: automatad.Builder, openrtb_ext.BidderAvocet: avocet.Builder, + openrtb_ext.BidderAxis: axis.Builder, openrtb_ext.BidderAxonix: axonix.Builder, openrtb_ext.BidderBeachfront: beachfront.Builder, openrtb_ext.BidderBeintoo: beintoo.Builder, + openrtb_ext.BidderBematterfull: bematterfull.Builder, openrtb_ext.BidderBetween: between.Builder, openrtb_ext.BidderBeyondMedia: beyondmedia.Builder, openrtb_ext.BidderBidmachine: bidmachine.Builder, @@ -224,10 +249,11 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderBizzclick: bizzclick.Builder, openrtb_ext.BidderBliink: bliink.Builder, openrtb_ext.BidderBlue: blue.Builder, + openrtb_ext.BidderBluesea: bluesea.Builder, openrtb_ext.BidderBmtm: bmtm.Builder, openrtb_ext.BidderBoldwin: boldwin.Builder, openrtb_ext.BidderBrave: brave.Builder, - openrtb_ext.BidderBrightroll: brightroll.Builder, + openrtb_ext.BidderCadentApertureMX: cadentaperturemx.Builder, openrtb_ext.BidderCcx: ccx.Builder, openrtb_ext.BidderCoinzilla: coinzilla.Builder, openrtb_ext.BidderColossus: colossus.Builder, @@ -235,6 +261,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderConnectAd: connectad.Builder, openrtb_ext.BidderConsumable: consumable.Builder, openrtb_ext.BidderConversant: conversant.Builder, + openrtb_ext.BidderCopper6: adtelligent.Builder, openrtb_ext.BidderCpmstar: cpmstar.Builder, openrtb_ext.BidderCriteo: criteo.Builder, openrtb_ext.BidderCWire: cwire.Builder, @@ -244,19 +271,23 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderDefinemedia: definemedia.Builder, openrtb_ext.BidderDianomi: dianomi.Builder, openrtb_ext.BidderDmx: dmx.Builder, - openrtb_ext.BidderEmxDigital: emx_digital.Builder, + openrtb_ext.BidderEmtv: emtv.Builder, + openrtb_ext.BidderEmxDigital: cadentaperturemx.Builder, openrtb_ext.BidderEngageBDR: engagebdr.Builder, openrtb_ext.BidderEPlanning: eplanning.Builder, openrtb_ext.BidderEpom: epom.Builder, + openrtb_ext.BidderEpsilon: conversant.Builder, openrtb_ext.BidderEVolution: evolution.Builder, openrtb_ext.BidderEvtech: limelightDigital.Builder, + openrtb_ext.BidderFlipp: flipp.Builder, openrtb_ext.BidderFreewheelSSP: freewheelssp.Builder, openrtb_ext.BidderFreewheelSSPOld: freewheelssp.Builder, + openrtb_ext.BidderFRVRAdNetwork: frvradn.Builder, openrtb_ext.BidderGamma: gamma.Builder, openrtb_ext.BidderGamoshi: gamoshi.Builder, openrtb_ext.BidderGlobalsun: globalsun.Builder, + openrtb_ext.BidderGothamads: gothamads.Builder, openrtb_ext.BidderGrid: grid.Builder, - openrtb_ext.BidderGroupm: pubmatic.Builder, openrtb_ext.BidderGumGum: gumgum.Builder, openrtb_ext.BidderHuaweiAds: huaweiads.Builder, openrtb_ext.BidderIionads: limelightDigital.Builder, @@ -275,8 +306,10 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderKayzen: kayzen.Builder, openrtb_ext.BidderKidoz: kidoz.Builder, openrtb_ext.BidderKiviads: kiviads.Builder, + openrtb_ext.BidderLmKiviads: lmkiviads.Builder, openrtb_ext.BidderKrushmedia: krushmedia.Builder, openrtb_ext.BidderKubient: kubient.Builder, + openrtb_ext.BidderLiftoff: liftoff.Builder, openrtb_ext.BidderLimelightDigital: limelightDigital.Builder, openrtb_ext.BidderLockerDome: lockerdome.Builder, openrtb_ext.BidderLogan: logan.Builder, @@ -288,8 +321,10 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderMediafuse: appnexus.Builder, openrtb_ext.BidderMedianet: medianet.Builder, openrtb_ext.BidderMgid: mgid.Builder, + openrtb_ext.BidderMgidX: mgidX.Builder, openrtb_ext.BidderMobfoxpb: mobfoxpb.Builder, openrtb_ext.BidderMobileFuse: mobilefuse.Builder, + openrtb_ext.BidderMotorik: motorik.Builder, openrtb_ext.BidderNanoInteractive: nanointeractive.Builder, openrtb_ext.BidderNextMillennium: nextmillennium.Builder, openrtb_ext.BidderNinthDecimal: ninthdecimal.Builder, @@ -300,6 +335,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderOperaads: operaads.Builder, openrtb_ext.BidderOrbidder: orbidder.Builder, openrtb_ext.BidderOutbrain: outbrain.Builder, + openrtb_ext.BidderOwnAdx: ownadx.Builder, openrtb_ext.BidderPangle: pangle.Builder, openrtb_ext.BidderPGAM: adtelligent.Builder, openrtb_ext.BidderPubmatic: pubmatic.Builder, @@ -310,12 +346,15 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderRevcontent: revcontent.Builder, openrtb_ext.BidderRhythmone: rhythmone.Builder, openrtb_ext.BidderRichaudience: richaudience.Builder, + openrtb_ext.BidderRise: rise.Builder, openrtb_ext.BidderRTBHouse: rtbhouse.Builder, openrtb_ext.BidderRubicon: rubicon.Builder, openrtb_ext.BidderSeedingAlliance: seedingAlliance.Builder, openrtb_ext.BidderSaLunaMedia: salunamedia.Builder, + openrtb_ext.BidderScreencore: screencore.Builder, openrtb_ext.BidderSharethrough: sharethrough.Builder, openrtb_ext.BidderSilverMob: silvermob.Builder, + openrtb_ext.BidderSilverPush: silverpush.Builder, openrtb_ext.BidderSmaato: smaato.Builder, openrtb_ext.BidderSmartAdserver: smartadserver.Builder, openrtb_ext.BidderSmartHub: smarthub.Builder, @@ -332,6 +371,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderTaboola: taboola.Builder, openrtb_ext.BidderTappx: tappx.Builder, openrtb_ext.BidderTelaria: telaria.Builder, + openrtb_ext.BidderTpmn: tpmn.Builder, openrtb_ext.BidderTrafficGate: trafficgate.Builder, openrtb_ext.BidderTriplelift: triplelift.Builder, openrtb_ext.BidderTripleliftNative: triplelift_native.Builder, @@ -341,19 +381,24 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderUnicorn: unicorn.Builder, openrtb_ext.BidderUnruly: unruly.Builder, openrtb_ext.BidderValueImpression: apacdex.Builder, - openrtb_ext.BidderVerizonMedia: yahoossp.Builder, openrtb_ext.BidderVideoByte: videobyte.Builder, openrtb_ext.BidderVideoHeroes: videoheroes.Builder, openrtb_ext.BidderVidoomy: vidoomy.Builder, openrtb_ext.BidderViewdeos: adtelligent.Builder, openrtb_ext.BidderVisibleMeasures: visiblemeasures.Builder, openrtb_ext.BidderVisx: visx.Builder, + openrtb_ext.BidderVox: vox.Builder, openrtb_ext.BidderVrtcal: vrtcal.Builder, - openrtb_ext.BidderYahooSSP: yahoossp.Builder, + openrtb_ext.BidderXeworks: xeworks.Builder, + openrtb_ext.BidderXtrmqb: limelightDigital.Builder, + openrtb_ext.BidderYahooAds: yahooAds.Builder, + openrtb_ext.BidderYahooAdvertising: yahooAds.Builder, + openrtb_ext.BidderYahooSSP: yahooAds.Builder, openrtb_ext.BidderYeahmobi: yeahmobi.Builder, openrtb_ext.BidderYieldlab: yieldlab.Builder, openrtb_ext.BidderYieldmo: yieldmo.Builder, openrtb_ext.BidderYieldone: yieldone.Builder, openrtb_ext.BidderZeroClickFraud: zeroclickfraud.Builder, + openrtb_ext.BidderZetaGlobalSsp: zeta_global_ssp.Builder, } } diff --git a/exchange/adapter_util.go b/exchange/adapter_util.go index 056dbf3568e..ee9a066aa58 100644 --- a/exchange/adapter_util.go +++ b/exchange/adapter_util.go @@ -33,12 +33,23 @@ func buildBidders(infos config.BidderInfos, builders map[openrtb_ext.BidderName] var errs []error for bidder, info := range infos { + if len(info.AliasOf) > 0 { + errs = append(errs, fmt.Errorf("This feature is currently under development")) + continue + } bidderName, bidderNameFound := openrtb_ext.NormalizeBidderName(bidder) if !bidderNameFound { errs = append(errs, fmt.Errorf("%v: unknown bidder", bidder)) continue } + if len(info.AliasOf) > 0 { + if err := setAliasBuilder(info, builders, bidderName); err != nil { + errs = append(errs, fmt.Errorf("%v: failed to set alias builder: %v", bidder, err)) + continue + } + } + builder, builderFound := builders[bidderName] if !builderFound { errs = append(errs, fmt.Errorf("%v: builder not registered", bidder)) @@ -59,6 +70,20 @@ func buildBidders(infos config.BidderInfos, builders map[openrtb_ext.BidderName] return bidders, errs } +func setAliasBuilder(info config.BidderInfo, builders map[openrtb_ext.BidderName]adapters.Builder, bidderName openrtb_ext.BidderName) error { + parentBidderName, parentBidderFound := openrtb_ext.NormalizeBidderName(info.AliasOf) + if !parentBidderFound { + return fmt.Errorf("unknown parent bidder: %v for alias: %v", info.AliasOf, bidderName) + } + + builder, builderFound := builders[parentBidderName] + if !builderFound { + return fmt.Errorf("%v: parent builder not registered", parentBidderName) + } + builders[bidderName] = builder + return nil +} + func buildAdapterInfo(bidderInfo config.BidderInfo) config.Adapter { adapter := config.Adapter{} adapter.Endpoint = bidderInfo.Endpoint @@ -82,17 +107,25 @@ func GetActiveBidders(infos config.BidderInfos) map[string]openrtb_ext.BidderNam return activeBidders } -// GetDisabledBiddersErrorMessages returns a map of error messages for disabled bidders. -func GetDisabledBiddersErrorMessages(infos config.BidderInfos) map[string]string { - disabledBidders := map[string]string{ +func GetDisabledBidderWarningMessages(infos config.BidderInfos) map[string]string { + removed := map[string]string{ "lifestreet": `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`, "adagio": `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`, "somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`, - "yssp": `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`, + "yssp": `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahooAds" in your configuration.`, "andbeyondmedia": `Bidder "andbeyondmedia" is no longer available in Prebid Server. If you're looking to use the AndBeyond.Media SSP adapter, please rename it to "beyondmedia" in your configuration.`, "oftmedia": `Bidder "oftmedia" is no longer available in Prebid Server. Please update your configuration.`, + "groupm": `Bidder "groupm" is no longer available in Prebid Server. Please update your configuration.`, + "verizonmedia": `Bidder "verizonmedia" is no longer available in Prebid Server. Please update your configuration.`, + "brightroll": `Bidder "brightroll" is no longer available in Prebid Server. Please update your configuration.`, } + return mergeRemovedAndDisabledBidderWarningMessages(removed, infos) +} + +func mergeRemovedAndDisabledBidderWarningMessages(removed map[string]string, infos config.BidderInfos) map[string]string { + disabledBidders := removed + for name, info := range infos { if info.Disabled { msg := fmt.Sprintf(`Bidder "%s" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`, name) diff --git a/exchange/adapter_util_test.go b/exchange/adapter_util_test.go index 4a6f468ecdc..611498fea0c 100644 --- a/exchange/adapter_util_test.go +++ b/exchange/adapter_util_test.go @@ -13,6 +13,7 @@ import ( metrics "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -67,6 +68,13 @@ func TestBuildAdapters(t *testing.T) { errors.New("unknown: unknown bidder"), }, }, + { + description: "Alias feature disabled", + bidderInfos: map[string]config.BidderInfo{"appNexus": {AliasOf: "rubicon"}}, + expectedErrors: []error{ + errors.New("This feature is currently under development"), + }, + }, } cfg := &config.Configuration{} @@ -163,6 +171,55 @@ func TestBuildBidders(t *testing.T) { } } +func TestSetAliasBuilder(t *testing.T) { + rubiconBidder := fakeBidder{"b"} + ixBidder := fakeBidder{"ix"} + rubiconBuilder := fakeBuilder{rubiconBidder, nil}.Builder + ixBuilder := fakeBuilder{ixBidder, nil}.Builder + + testCases := []struct { + description string + bidderInfo config.BidderInfo + builders map[openrtb_ext.BidderName]adapters.Builder + bidderName openrtb_ext.BidderName + expectedBuilders map[openrtb_ext.BidderName]adapters.Builder + expectedError error + }{ + { + description: "Success - Alias builder", + bidderInfo: config.BidderInfo{Disabled: false, AliasOf: "rubicon"}, + bidderName: openrtb_ext.BidderName("appnexus"), + builders: map[openrtb_ext.BidderName]adapters.Builder{openrtb_ext.BidderRubicon: rubiconBuilder}, + expectedBuilders: map[openrtb_ext.BidderName]adapters.Builder{openrtb_ext.BidderRubicon: rubiconBuilder, openrtb_ext.BidderAppnexus: rubiconBuilder}, + }, + { + description: "Failure - Invalid parent bidder builder", + bidderInfo: config.BidderInfo{Disabled: false, AliasOf: "rubicon"}, + bidderName: openrtb_ext.BidderName("appnexus"), + builders: map[openrtb_ext.BidderName]adapters.Builder{openrtb_ext.BidderIx: ixBuilder}, + expectedError: errors.New("rubicon: parent builder not registered"), + }, + { + description: "Failure - Invalid parent for alias", + bidderInfo: config.BidderInfo{Disabled: false, AliasOf: "unknown"}, + bidderName: openrtb_ext.BidderName("appnexus"), + builders: map[openrtb_ext.BidderName]adapters.Builder{openrtb_ext.BidderIx: ixBuilder}, + expectedError: errors.New("unknown parent bidder: unknown for alias: appnexus"), + }, + } + + for _, test := range testCases { + err := setAliasBuilder(test.bidderInfo, test.builders, test.bidderName) + + if test.expectedBuilders != nil { + assert.ObjectsAreEqual(test.builders, test.expectedBuilders) + } + if test.expectedError != nil { + assert.EqualError(t, test.expectedError, err.Error(), test.description+":errors") + } + } +} + func TestGetActiveBidders(t *testing.T) { testCases := []struct { description string @@ -197,67 +254,71 @@ func TestGetActiveBidders(t *testing.T) { } } -func TestGetDisabledBiddersErrorMessages(t *testing.T) { +func TestGetDisabledBidderWarningMessages(t *testing.T) { + t.Run("removed", func(t *testing.T) { + result := GetDisabledBidderWarningMessages(nil) + + // test proper construction by verifying one expected bidder is in the list + require.Contains(t, result, "groupm") + assert.Equal(t, result["groupm"], `Bidder "groupm" is no longer available in Prebid Server. Please update your configuration.`) + }) + + t.Run("removed-and-disabled", func(t *testing.T) { + result := GetDisabledBidderWarningMessages(map[string]config.BidderInfo{"bidderA": infoDisabled}) + + // test proper construction by verifying one expected bidder is in the list with the disabled bidder + require.Contains(t, result, "groupm") + assert.Equal(t, result["groupm"], `Bidder "groupm" is no longer available in Prebid Server. Please update your configuration.`) + + require.Contains(t, result, "bidderA") + assert.Equal(t, result["bidderA"], `Bidder "bidderA" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`) + }) +} + +func TestMergeRemovedAndDisabledBidderWarningMessages(t *testing.T) { testCases := []struct { - description string - bidderInfos map[string]config.BidderInfo - expected map[string]string + name string + givenRemoved map[string]string + givenBidderInfos map[string]config.BidderInfo + expected map[string]string }{ { - description: "None", - bidderInfos: map[string]config.BidderInfo{}, - expected: map[string]string{ - "lifestreet": `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`, - "adagio": `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`, - "somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`, - "yssp": `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`, - "andbeyondmedia": `Bidder "andbeyondmedia" is no longer available in Prebid Server. If you're looking to use the AndBeyond.Media SSP adapter, please rename it to "beyondmedia" in your configuration.`, - "oftmedia": `Bidder "oftmedia" is no longer available in Prebid Server. Please update your configuration.`, - }, + name: "none", + givenRemoved: map[string]string{}, + givenBidderInfos: map[string]config.BidderInfo{}, + expected: map[string]string{}, }, { - description: "Enabled", - bidderInfos: map[string]config.BidderInfo{"appnexus": infoEnabled}, - expected: map[string]string{ - "lifestreet": `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`, - "adagio": `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`, - "somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`, - "yssp": `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`, - "andbeyondmedia": `Bidder "andbeyondmedia" is no longer available in Prebid Server. If you're looking to use the AndBeyond.Media SSP adapter, please rename it to "beyondmedia" in your configuration.`, - "oftmedia": `Bidder "oftmedia" is no longer available in Prebid Server. Please update your configuration.`, - }, + name: "removed", + givenRemoved: map[string]string{"bidderA": `Bidder A Message`}, + givenBidderInfos: map[string]config.BidderInfo{}, + expected: map[string]string{"bidderA": `Bidder A Message`}, }, { - description: "Disabled", - bidderInfos: map[string]config.BidderInfo{"appnexus": infoDisabled}, - expected: map[string]string{ - "lifestreet": `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`, - "adagio": `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`, - "somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`, - "yssp": `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`, - "appnexus": `Bidder "appnexus" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`, - "andbeyondmedia": `Bidder "andbeyondmedia" is no longer available in Prebid Server. If you're looking to use the AndBeyond.Media SSP adapter, please rename it to "beyondmedia" in your configuration.`, - "oftmedia": `Bidder "oftmedia" is no longer available in Prebid Server. Please update your configuration.`, - }, + name: "enabled", + givenRemoved: map[string]string{}, + givenBidderInfos: map[string]config.BidderInfo{"bidderA": infoEnabled}, + expected: map[string]string{}, }, { - description: "Mixed", - bidderInfos: map[string]config.BidderInfo{"appnexus": infoDisabled, "openx": infoEnabled}, - expected: map[string]string{ - "lifestreet": `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`, - "adagio": `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`, - "somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`, - "yssp": `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`, - "appnexus": `Bidder "appnexus" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`, - "andbeyondmedia": `Bidder "andbeyondmedia" is no longer available in Prebid Server. If you're looking to use the AndBeyond.Media SSP adapter, please rename it to "beyondmedia" in your configuration.`, - "oftmedia": `Bidder "oftmedia" is no longer available in Prebid Server. Please update your configuration.`, - }, + name: "disabled", + givenRemoved: map[string]string{}, + givenBidderInfos: map[string]config.BidderInfo{"bidderA": infoDisabled}, + expected: map[string]string{"bidderA": `Bidder "bidderA" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`}, + }, + { + name: "mixed", + givenRemoved: map[string]string{"bidderA": `Bidder A Message`}, + givenBidderInfos: map[string]config.BidderInfo{"bidderB": infoEnabled, "bidderC": infoDisabled}, + expected: map[string]string{"bidderA": `Bidder A Message`, "bidderC": `Bidder "bidderC" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`}, }, } for _, test := range testCases { - result := GetDisabledBiddersErrorMessages(test.bidderInfos) - assert.Equal(t, test.expected, result, test.description) + t.Run(test.name, func(t *testing.T) { + result := mergeRemovedAndDisabledBidderWarningMessages(test.givenRemoved, test.givenBidderInfos) + assert.Equal(t, test.expected, result, test.name) + }) } } diff --git a/exchange/auction.go b/exchange/auction.go index 330aa50c56f..b2d6eb571ca 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -185,12 +185,12 @@ func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderNa } } -func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity) { +func (a *auction) setRoundedPrices(targetingData targetData) { roundedPrices := make(map[*entities.PbsOrtbBid]string, 5*len(a.winningBids)) for _, topBidsPerImp := range a.winningBidsByBidder { for _, topBidsPerBidder := range topBidsPerImp { for _, topBid := range topBidsPerBidder { - roundedPrices[topBid] = GetPriceBucket(topBid.Bid.Price, priceGranularity) + roundedPrices[topBid] = GetPriceBucket(*topBid.Bid, targetingData) } } } diff --git a/exchange/auction_response.go b/exchange/auction_response.go new file mode 100644 index 00000000000..3b85a4472c2 --- /dev/null +++ b/exchange/auction_response.go @@ -0,0 +1,20 @@ +package exchange + +import ( + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// AuctionResponse contains OpenRTB Bid Response object and its extension (un-marshalled) object +type AuctionResponse struct { + *openrtb2.BidResponse + ExtBidResponse *openrtb_ext.ExtBidResponse +} + +// GetSeatNonBid returns array of seat non-bid if present. nil otherwise +func (ar *AuctionResponse) GetSeatNonBid() []openrtb_ext.SeatNonBid { + if ar != nil && ar.ExtBidResponse != nil && ar.ExtBidResponse.Prebid != nil { + return ar.ExtBidResponse.Prebid.SeatNonBid + } + return nil +} diff --git a/exchange/bidder.go b/exchange/bidder.go index f2cf790fac6..838c46f73d1 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -16,6 +16,7 @@ import ( "time" "github.com/golang/glog" + "github.com/prebid/prebid-server/bidadjustment" "github.com/prebid/prebid-server/config/util" "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/exchange/entities" @@ -23,6 +24,7 @@ import ( "github.com/prebid/prebid-server/hooks/hookexecution" "github.com/prebid/prebid-server/version" + "github.com/prebid/openrtb/v19/adcom1" nativeRequests "github.com/prebid/openrtb/v19/native1/request" nativeResponse "github.com/prebid/openrtb/v19/native1/response" "github.com/prebid/openrtb/v19/openrtb2" @@ -56,15 +58,27 @@ type AdaptedBidder interface { // // Any errors will be user-facing in the API. // Error messages should help publishers understand what might account for "bad" bids. - requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) + requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) } // bidRequestOptions holds additional options for bid request execution to maintain clean code and reasonable number of parameters type bidRequestOptions struct { - accountDebugAllowed bool - headerDebugAllowed bool - addCallSignHeader bool - bidAdjustments map[string]float64 + accountDebugAllowed bool + headerDebugAllowed bool + addCallSignHeader bool + bidAdjustments map[string]float64 + tmaxAdjustments *TmaxAdjustmentsPreprocessed + bidderRequestStartTime time.Time +} + +type extraBidderRespInfo struct { + respProcessingStartTime time.Time +} + +type extraAuctionResponseInfo struct { + fledge *openrtb_ext.Fledge + bidsFound bool + bidderResponseStartTime time.Time } const ImpIdReqBody = "Stored bid response for impression id: " @@ -74,6 +88,8 @@ const ( Gzip string = "GZIP" ) +var errTmaxTimeout = errors.New("exceeded tmax duration") + // AdaptBidder converts an adapters.Bidder into an exchange.AdaptedBidder. // // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter" @@ -115,19 +131,27 @@ type bidderAdapterConfig struct { EndpointCompression string } -func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) { +func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { reject := hookExecutor.ExecuteBidderRequestStage(bidderRequest.BidRequest, string(bidderRequest.BidderName)) if reject != nil { - return nil, []error{reject} + return nil, extraBidderRespInfo{}, []error{reject} } - var reqData []*adapters.RequestData - var errs []error - var responseChannel chan *httpCallInfo + var ( + reqData []*adapters.RequestData + errs []error + responseChannel chan *httpCallInfo + extraRespInfo extraBidderRespInfo + ) //check if real request exists for this bidder or it only has stored responses dataLen := 0 if len(bidderRequest.BidRequest.Imp) > 0 { + // Reducing the amount of time bidders have to compensate for the processing time used by PBS to fetch a stored request (if needed), validate the OpenRTB request and split it into multiple requests sanitized for each bidder + // As well as for the time needed by PBS to prepare the auction response + if bidRequestOptions.tmaxAdjustments != nil && bidRequestOptions.tmaxAdjustments.IsEnforced { + bidderRequest.BidRequest.TMax = getBidderTmax(&bidderTmaxCtx{ctx}, bidderRequest.BidRequest.TMax, *bidRequestOptions.tmaxAdjustments) + } reqData, errs = bidder.Bidder.MakeRequests(bidderRequest.BidRequest, reqInfo) if len(reqData) == 0 { @@ -135,7 +159,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde if len(errs) == 0 { errs = append(errs, &errortypes.FailedToRequestBids{Message: "The adapter failed to generate any bid requests, but also failed to generate an error explaining why"}) } - return nil, errs + return nil, extraBidderRespInfo{}, errs } xPrebidHeader := version.BuildXPrebidHeaderForRequest(bidderRequest.BidRequest, version.Ver) @@ -169,11 +193,11 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde dataLen = len(reqData) + len(bidderRequest.BidderStoredResponses) responseChannel = make(chan *httpCallInfo, dataLen) if len(reqData) == 1 { - responseChannel <- bidder.doRequest(ctx, reqData[0]) + responseChannel <- bidder.doRequest(ctx, reqData[0], bidRequestOptions.bidderRequestStartTime, bidRequestOptions.tmaxAdjustments) } else { for _, oneReqData := range reqData { go func(data *adapters.RequestData) { - responseChannel <- bidder.doRequest(ctx, data) + responseChannel <- bidder.doRequest(ctx, data, bidRequestOptions.bidderRequestStartTime, bidRequestOptions.tmaxAdjustments) }(oneReqData) // Method arg avoids a race condition on oneReqData } } @@ -227,6 +251,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } if httpInfo.err == nil { + extraRespInfo.respProcessingStartTime = time.Now() bidResponse, moreErrs := bidder.Bidder.MakeBids(bidderRequest.BidRequest, httpInfo.request, httpInfo.response) errs = append(errs, moreErrs...) @@ -332,9 +357,13 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } originalBidCpm := 0.0 + currencyAfterAdjustments := "" if bidResponse.Bids[i].Bid != nil { originalBidCpm = bidResponse.Bids[i].Bid.Price bidResponse.Bids[i].Bid.Price = bidResponse.Bids[i].Bid.Price * adjustmentFactor * conversionRate + + bidType := getBidTypeForAdjustments(bidResponse.Bids[i].BidType, bidResponse.Bids[i].Bid.ImpID, bidderRequest.BidRequest.Imp) + bidResponse.Bids[i].Bid.Price, currencyAfterAdjustments = bidadjustment.Apply(ruleToAdjustments, bidResponse.Bids[i], bidderRequest.BidderName, seatBidMap[bidderRequest.BidderName].Currency, reqInfo, bidType) } if _, ok := seatBidMap[bidderName]; !ok { @@ -357,6 +386,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde OriginalBidCPM: originalBidCpm, OriginalBidCur: bidResponse.Currency, }) + seatBidMap[bidderName].Currency = currencyAfterAdjustments } } else { // If no conversions found, do not handle the bid @@ -367,13 +397,12 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde errs = append(errs, httpInfo.err) } } - seatBids := make([]*entities.PbsOrtbSeatBid, 0, len(seatBidMap)) for _, seatBid := range seatBidMap { seatBids = append(seatBids, seatBid) } - return seatBids, errs + return seatBids, extraRespInfo, errs } func addNativeTypes(bid *openrtb2.Bid, request *openrtb2.BidRequest) (*nativeResponse.Response, []error) { @@ -487,11 +516,11 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { // doRequest makes a request, handles the response, and returns the data needed by the // Bidder interface. -func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData) *httpCallInfo { - return bidder.doRequestImpl(ctx, req, glog.Warningf) +func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { + return bidder.doRequestImpl(ctx, req, glog.Warningf, bidderRequestStartTime, tmaxAdjustments) } -func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg) *httpCallInfo { +func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { var requestBody []byte switch strings.ToUpper(bidder.config.EndpointCompression) { @@ -515,6 +544,19 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re if !bidder.config.DisableConnMetrics { ctx = bidder.addClientTrace(ctx) } + bidder.me.RecordOverheadTime(metrics.PreBidder, time.Since(bidderRequestStartTime)) + + if tmaxAdjustments != nil && tmaxAdjustments.IsEnforced { + if hasShorterDurationThanTmax(&bidderTmaxCtx{ctx}, *tmaxAdjustments) { + bidder.me.RecordTMaxTimeout() + return &httpCallInfo{ + request: req, + err: errTmaxTimeout, + } + } + } + + httpCallStart := time.Now() httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq) if err != nil { if err == context.DeadlineExceeded { @@ -555,6 +597,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re } } + bidder.me.RecordBidderServerResponseTime(time.Since(httpCallStart)) return &httpCallInfo{ request: req, response: &adapters.ResponseData{ @@ -680,3 +723,31 @@ func compressToGZIP(requestBody []byte) []byte { w.Close() return b.Bytes() } + +func getBidTypeForAdjustments(bidType openrtb_ext.BidType, impID string, imp []openrtb2.Imp) string { + if bidType == openrtb_ext.BidTypeVideo { + for _, imp := range imp { + if imp.ID == impID { + if imp.Video != nil && imp.Video.Plcmt == adcom1.VideoPlcmtAccompanyingContent { + return "video-outstream" + } + break + } + } + return "video-instream" + } + return string(bidType) +} + +func hasShorterDurationThanTmax(ctx bidderTmaxContext, tmaxAdjustments TmaxAdjustmentsPreprocessed) bool { + if tmaxAdjustments.IsEnforced { + if deadline, ok := ctx.Deadline(); ok { + overheadNS := time.Duration(tmaxAdjustments.BidderNetworkLatencyBuffer+tmaxAdjustments.PBSResponsePreparationDuration) * time.Millisecond + bidderTmax := deadline.Add(-overheadNS) + + remainingDuration := ctx.Until(bidderTmax).Milliseconds() + return remainingDuration < int64(tmaxAdjustments.BidderResponseDurationMin) + } + } + return false +} diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index dca2b4f705b..e2d476c0411 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -17,6 +17,7 @@ import ( "time" "github.com/golang/glog" + "github.com/prebid/openrtb/v19/adcom1" nativeRequests "github.com/prebid/openrtb/v19/native1/request" nativeResponse "github.com/prebid/openrtb/v19/native1/response" "github.com/prebid/openrtb/v19/openrtb2" @@ -108,8 +109,11 @@ func TestSingleBidder(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + extraInfo := &adapters.ExtraRequestInfo{} + seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) + assert.Len(t, seatBids, 1) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) seatBid := seatBids[0] // Make sure the goodSingleBidder was called with the expected arguments. @@ -230,8 +234,10 @@ func TestSingleBidderGzip(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + extraInfo := &adapters.ExtraRequestInfo{} + seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) assert.Len(t, seatBids, 1) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) seatBid := seatBids[0] // Make sure the goodSingleBidder was called with the expected arguments. @@ -330,8 +336,8 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) - + extraInfo := &adapters.ExtraRequestInfo{} + seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) expectedHttpCalls := []*openrtb_ext.ExtHttpCall{ { Uri: server.URL, @@ -344,6 +350,7 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) { assert.Empty(t, errs) assert.Len(t, seatBids, 1) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCalls) } @@ -382,7 +389,8 @@ func TestSetGPCHeader(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"} + seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) expectedHttpCall := []*openrtb_ext.ExtHttpCall{ { @@ -396,6 +404,7 @@ func TestSetGPCHeader(t *testing.T) { assert.Empty(t, errs) assert.Len(t, seatBids, 1) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall) } @@ -432,7 +441,8 @@ func TestSetGPCHeaderNil(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"} + seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) expectedHttpCall := []*openrtb_ext.ExtHttpCall{ { @@ -446,6 +456,7 @@ func TestSetGPCHeaderNil(t *testing.T) { assert.Empty(t, errs) assert.Len(t, seatBids, 1) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall) } @@ -502,7 +513,7 @@ func TestMultiBidder(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) if len(seatBids) != 1 { t.Fatalf("SeatBid should exist, because bids exist.") @@ -514,6 +525,7 @@ func TestMultiBidder(t *testing.T) { if len(seatBids[0].Bids) != len(bidderImpl.httpResponses)*len(mockBidderResponse.Bids) { t.Errorf("Expected %d bids. Got %d", len(bidderImpl.httpResponses)*len(mockBidderResponse.Bids), len(seatBids[0].Bids)) } + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } @@ -543,11 +555,11 @@ func TestBidderTimeout(t *testing.T) { Client: server.Client(), me: &metricsConfig.NilMetricsEngine{}, } - + tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} callInfo := bidder.doRequest(ctx, &adapters.RequestData{ Method: "POST", Uri: server.URL, - }) + }, time.Now(), tmaxAdjustments) if callInfo.err == nil { t.Errorf("The bidder should report an error if the context has expired already.") } @@ -563,10 +575,10 @@ func TestInvalidRequest(t *testing.T) { Bidder: &mixedMultiBidder{}, Client: server.Client(), } - + tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{ Method: "\"", // force http.NewRequest() to fail - }) + }, time.Now(), tmaxAdjustments) if callInfo.err == nil { t.Errorf("bidderAdapter.doRequest should return an error if the request data is malformed.") } @@ -586,11 +598,11 @@ func TestConnectionClose(t *testing.T) { BidderName: openrtb_ext.BidderAppnexus, me: &metricsConfig.NilMetricsEngine{}, } - + tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{ Method: "POST", Uri: server.URL, - }) + }, time.Now(), tmaxAdjustments) if callInfo.err == nil { t.Errorf("bidderAdapter.doRequest should return an error if the connection closes unexpectedly.") } @@ -869,7 +881,7 @@ func TestMultiCurrencies(t *testing.T) { BidderName: openrtb_ext.BidderAppnexus, } bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 1} - seatBids, errs := bidder.requestBid( + seatBids, extraBidderRespInfo, errs := bidder.requestBid( context.Background(), bidderReq, currencyConverter.Rates(), @@ -883,9 +895,11 @@ func TestMultiCurrencies(t *testing.T) { }, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, + nil, ) assert.Len(t, seatBids, 1) seatBid := seatBids[0] + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) // Verify: resultLightBids := make([]bid, len(seatBid.Bids)) @@ -1028,7 +1042,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { BidderName: "test", } bidAdjustments := map[string]float64{"test": 1} - seatBids, errs := bidder.requestBid( + seatBids, extraBidderRespInfo, errs := bidder.requestBid( context.Background(), bidderReq, currencyConverter.Rates(), @@ -1042,6 +1056,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { }, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, + nil, ) assert.Len(t, seatBids, 1) seatBid := seatBids[0] @@ -1050,6 +1065,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { assert.Equal(t, false, (seatBid == nil && tc.expectedBidsCount != 0), tc.description) assert.Equal(t, tc.expectedBidsCount, uint(len(seatBid.Bids)), tc.description) assert.ElementsMatch(t, tc.expectedBadCurrencyErrors, errs, tc.description) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } } @@ -1206,7 +1222,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { BidderName: "test", } bidAdjustments := map[string]float64{"test": 1} - seatBids, errs := bidder.requestBid( + seatBids, extraBidderRespInfo, errs := bidder.requestBid( context.Background(), bidderReq, currencyConverter.Rates(), @@ -1220,6 +1236,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { }, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, + nil, ) assert.Len(t, seatBids, 1) seatBid := seatBids[0] @@ -1230,6 +1247,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { } else { assert.Nil(t, errs, tc.description) assert.Equal(t, tc.expectedPickedCurrency, seatBid.Currency, tc.description) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } } } @@ -1523,7 +1541,7 @@ func TestMobileNativeTypes(t *testing.T) { BidderName: "test", } bidAdjustments := map[string]float64{"test": 1.0} - seatBids, _ := bidder.requestBid( + seatBids, extraBidderRespInfo, _ := bidder.requestBid( context.Background(), bidderReq, currencyConverter.Rates(), @@ -1537,9 +1555,10 @@ func TestMobileNativeTypes(t *testing.T) { }, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, + nil, ) assert.Len(t, seatBids, 1) - + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) var actualValue string for _, bid := range seatBids[0].Bids { actualValue = bid.Bid.AdM @@ -1642,7 +1661,7 @@ func TestRequestBidsStoredBidResponses(t *testing.T) { ImpReplaceImpId: tc.impReplaceImpId, } bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 1.0} - seatBids, _ := bidder.requestBid( + seatBids, extraBidderRespInfo, _ := bidder.requestBid( context.Background(), bidderReq, currencyConverter.Rates(), @@ -1656,8 +1675,10 @@ func TestRequestBidsStoredBidResponses(t *testing.T) { }, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, + nil, ) assert.Len(t, seatBids, 1) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) assert.Len(t, seatBids[0].Bids, len(tc.expectedBidIds), "Incorrect bids number for test case ", tc.description) for _, bid := range seatBids[0].Bids { @@ -1756,7 +1777,7 @@ func TestFledge(t *testing.T) { }, BidderName: "openx", } - seatBids, _ := bidder.requestBid( + seatBids, extraBidderRespInfo, _ := bidder.requestBid( context.Background(), bidderReq, currencyConverter.Rates(), @@ -1770,12 +1791,14 @@ func TestFledge(t *testing.T) { }, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, + nil, ) assert.Len(t, seatBids, 1) assert.NotNil(t, seatBids[0].FledgeAuctionConfigs) assert.Len(t, seatBids[0].FledgeAuctionConfigs, len(tc.expectedFledge)) assert.ElementsMatch(t, seatBids[0].FledgeAuctionConfigs, tc.expectedFledge) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } } @@ -1793,7 +1816,7 @@ func TestErrorReporting(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - bids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + bids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) if bids != nil { t.Errorf("There should be no seatbid if no http requests are returned.") } @@ -1803,6 +1826,7 @@ func TestErrorReporting(t *testing.T) { if errs[0].Error() != "Invalid params on BidRequest." { t.Errorf(`Error message was mutated. Expected "%s", Got "%s"`, "Invalid params on BidRequest.", errs[0].Error()) } + assert.True(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } func TestSetAssetTypes(t *testing.T) { @@ -2004,15 +2028,17 @@ func TestCallRecordAdapterConnections(t *testing.T) { bidResponse: &adapters.BidderResponse{}, } - // setup a mock metrics engine and its expectation - metrics := &metrics.MetricsEngineMock{} + // setup a mock mockMetricEngine engine and its expectation + mockMetricEngine := &metrics.MetricsEngineMock{} expectedAdapterName := openrtb_ext.BidderAppnexus compareConnWaitTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 } - metrics.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once() + mockMetricEngine.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once() + mockMetricEngine.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() + mockMetricEngine.On("RecordBidderServerResponseTime", mock.Anything).Once() // Run requestBid using an http.Client with a mock handler - bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, metrics, openrtb_ext.BidderAppnexus, nil, "") + bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, mockMetricEngine, openrtb_ext.BidderAppnexus, nil, "") currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) bidderReq := BidderRequest{ @@ -2025,13 +2051,13 @@ func TestCallRecordAdapterConnections(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - _, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + _, _, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) // Assert no errors assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs) // Assert RecordAdapterConnections() was called with the parameters we expected - metrics.AssertExpectations(t) + mockMetricEngine.AssertExpectations(t) } type DNSDoneTripper struct{} @@ -2070,6 +2096,8 @@ func TestCallRecordDNSTime(t *testing.T) { // setup a mock metrics engine and its expectation metricsMock := &metrics.MetricsEngineMock{} metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return() + metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() + metricsMock.On("RecordBidderServerResponseTime", mock.Anything).Once() // Instantiate the bidder that will send the request. We'll make sure to use an // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) @@ -2079,9 +2107,10 @@ func TestCallRecordDNSTime(t *testing.T) { Client: &http.Client{Transport: DNSDoneTripper{}}, me: metricsMock, } + tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} // Run test - bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}) + bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}, time.Now(), tmaxAdjustments) // Tried one or another, none seem to work without panicking metricsMock.AssertExpectations(t) @@ -2091,6 +2120,8 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) { // setup a mock metrics engine and its expectation metricsMock := &metrics.MetricsEngineMock{} metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return() + metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() + metricsMock.On("RecordBidderServerResponseTime", mock.Anything).Once() // Instantiate the bidder that will send the request. We'll make sure to use an // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) @@ -2100,9 +2131,10 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) { Client: &http.Client{Transport: TLSHandshakeTripper{}}, me: metricsMock, } + tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} // Run test - bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}) + bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}, time.Now(), tmaxAdjustments) // Tried one or another, none seem to work without panicking metricsMock.AssertExpectations(t) @@ -2189,8 +2221,8 @@ func TestTimeoutNotificationOn(t *testing.T) { logger := func(msg string, args ...interface{}) { loggerBuffer.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...)) } - - bidderAdapter.doRequestImpl(ctx, &bidRequest, logger) + tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} + bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, time.Now(), tmaxAdjustments) // Wait a little longer than the 205ms mock server sleep. time.Sleep(210 * time.Millisecond) @@ -2268,7 +2300,7 @@ func TestRequestBidsWithAdsCertsSigner(t *testing.T) { addCallSignHeader: true, bidAdjustments: bidAdjustments, } - _, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + _, _, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) assert.Empty(t, errs, "no errors should be returned") } @@ -2482,7 +2514,7 @@ func TestExtraBid(t *testing.T) { bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, + seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{ Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ @@ -2492,13 +2524,15 @@ func TestExtraBid(t *testing.T) { }, }, }, - &hookexecution.EmptyHookExecutor{}) + &hookexecution.EmptyHookExecutor{}, + nil) assert.Nil(t, errs) assert.Len(t, seatBids, 2) sort.Slice(seatBids, func(i, j int) bool { return len(seatBids[i].Seat) < len(seatBids[j].Seat) }) assert.Equal(t, wantSeatBids, seatBids) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) { @@ -2595,7 +2629,7 @@ func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) { bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, + seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{ Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ @@ -2605,10 +2639,12 @@ func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) { }, }, }, - &hookexecution.EmptyHookExecutor{}) + &hookexecution.EmptyHookExecutor{}, + nil) assert.Equal(t, wantErrs, errs) assert.Len(t, seatBids, 2) assert.ElementsMatch(t, wantSeatBids, seatBids) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } func TestExtraBidWithBidAdjustments(t *testing.T) { @@ -2695,7 +2731,7 @@ func TestExtraBidWithBidAdjustments(t *testing.T) { } bidAdjustments := map[string]float64{ string(openrtb_ext.BidderPubmatic): 2, - string(openrtb_ext.BidderGroupm): 3, + "groupm": 3, } bidReqOptions := bidRequestOptions{ @@ -2705,7 +2741,7 @@ func TestExtraBidWithBidAdjustments(t *testing.T) { bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, + seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{ Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ @@ -2715,13 +2751,15 @@ func TestExtraBidWithBidAdjustments(t *testing.T) { }, }, }, - &hookexecution.EmptyHookExecutor{}) + &hookexecution.EmptyHookExecutor{}, + nil) assert.Nil(t, errs) assert.Len(t, seatBids, 2) sort.Slice(seatBids, func(i, j int) bool { return len(seatBids[i].Seat) < len(seatBids[j].Seat) }) assert.Equal(t, wantSeatBids, seatBids) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) { @@ -2817,7 +2855,7 @@ func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) { bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, + seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{ Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ @@ -2827,13 +2865,15 @@ func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) { }, }, }, - &hookexecution.EmptyHookExecutor{}) + &hookexecution.EmptyHookExecutor{}, + nil) assert.Nil(t, errs) assert.Len(t, seatBids, 2) sort.Slice(seatBids, func(i, j int) bool { return len(seatBids[i].Seat) < len(seatBids[j].Seat) }) assert.Equal(t, wantSeatBids, seatBids) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) } func TestExtraBidWithMultiCurrencies(t *testing.T) { @@ -2941,7 +2981,7 @@ func TestExtraBidWithMultiCurrencies(t *testing.T) { bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, + seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{ Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ @@ -2951,11 +2991,328 @@ func TestExtraBidWithMultiCurrencies(t *testing.T) { }, }, }, - &hookexecution.EmptyHookExecutor{}) + &hookexecution.EmptyHookExecutor{}, + nil) assert.Nil(t, errs) assert.Len(t, seatBids, 2) sort.Slice(seatBids, func(i, j int) bool { return len(seatBids[i].Seat) < len(seatBids[j].Seat) }) assert.Equal(t, wantSeatBids, seatBids) + assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) +} + +func TestGetBidType(t *testing.T) { + testCases := []struct { + name string + givenBidType openrtb_ext.BidType + givenImpId string + givenImp []openrtb2.Imp + expected string + }{ + { + name: "VideoInstream", + givenImp: []openrtb2.Imp{ + { + ID: "imp-id", + Video: &openrtb2.Video{ + Plcmt: adcom1.VideoPlcmtInstream, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeVideo, + givenImpId: "imp-id", + expected: "video-instream", + }, + { + name: "VideoOutstream", + givenImp: []openrtb2.Imp{ + { + ID: "imp-id", + Video: &openrtb2.Video{ + Plcmt: adcom1.VideoPlcmtAccompanyingContent, + }, + }, + }, + givenBidType: openrtb_ext.BidTypeVideo, + givenImpId: "imp-id", + expected: "video-outstream", + }, + { + name: "NonVideoBidType", + givenImp: []openrtb2.Imp{}, + givenBidType: openrtb_ext.BidTypeBanner, + givenImpId: "imp-id", + expected: string(openrtb_ext.BidTypeBanner), + }, + { + name: "VideoBidTypeImpVideoIsNil", + givenImp: []openrtb2.Imp{ + { + ID: "imp-id", + }, + }, + givenBidType: openrtb_ext.BidTypeVideo, + givenImpId: "imp-id", + expected: "video-instream", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actual := getBidTypeForAdjustments(test.givenBidType, test.givenImpId, test.givenImp) + assert.Equal(t, test.expected, actual, "Bid type doesn't match") + }) + } +} + +type mockBidderTmaxCtx struct { + startTime, deadline, now time.Time + ok bool +} + +func (m *mockBidderTmaxCtx) Deadline() (deadline time.Time, _ bool) { + return m.deadline, m.ok +} +func (m *mockBidderTmaxCtx) RemainingDurationMS(deadline time.Time) int64 { + return deadline.Sub(m.startTime).Milliseconds() +} + +func (m *mockBidderTmaxCtx) Until(t time.Time) time.Duration { + return t.Sub(m.now) +} + +func TestUpdateBidderTmax(t *testing.T) { + respStatus := 200 + respBody := "{\"bid\":false}" + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + + requestHeaders := http.Header{} + requestHeaders.Add("Content-Type", "application/json") + + currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) + var requestTmax int64 = 700 + + bidderReq := BidderRequest{ + BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}, TMax: requestTmax}, + BidderName: "test", + } + extraInfo := &adapters.ExtraRequestInfo{} + + tests := []struct { + description string + requestTmax int64 + tmaxAdjustments *TmaxAdjustmentsPreprocessed + assertFn func(actualTmax int64) bool + }{ + { + description: "tmax-is-not-enabled", + requestTmax: requestTmax, + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: false}, + assertFn: func(actualTmax int64) bool { + return requestTmax == actualTmax + }, + }, + { + description: "updates-bidder-tmax", + requestTmax: requestTmax, + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 50, PBSResponsePreparationDuration: 50}, + assertFn: func(actualTmax int64) bool { + return requestTmax > actualTmax + }, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{}, + } + + now := time.Now() + ctx, cancel := context.WithDeadline(context.Background(), now.Add(500*time.Millisecond)) + defer cancel() + bidReqOptions := bidRequestOptions{bidderRequestStartTime: now, tmaxAdjustments: test.tmaxAdjustments} + bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{Allow: false}, "") + _, _, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) + assert.Empty(t, errs) + assert.True(t, test.assertFn(bidderImpl.bidRequest.TMax)) + }) + } +} + +func TestHasShorterDurationThanTmax(t *testing.T) { + var requestTmaxMS int64 = 700 + requestTmaxNS := requestTmaxMS * int64(time.Millisecond) + startTime := time.Date(2023, 5, 30, 1, 0, 0, 0, time.UTC) + now := time.Date(2023, 5, 30, 1, 0, 0, int(200*time.Millisecond), time.UTC) + deadline := time.Date(2023, 5, 30, 1, 0, 0, int(requestTmaxNS), time.UTC) + ctx := &mockBidderTmaxCtx{startTime: startTime, deadline: deadline, now: now, ok: true} + + tests := []struct { + description string + ctx bidderTmaxContext + requestTmax int64 + tmaxAdjustments TmaxAdjustmentsPreprocessed + expected bool + }{ + { + description: "tmax-disabled", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: false}, + expected: false, + }, + { + description: "remaing-duration-greater-than-bidder-response-min", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 50, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 40}, + expected: false, + }, + { + description: "remaing-duration-less-than-bidder-response-min", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 500}, + expected: true, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + assert.Equal(t, test.expected, hasShorterDurationThanTmax(test.ctx, test.tmaxAdjustments)) + }) + } +} + +func TestDoRequestImplWithTmax(t *testing.T) { + respStatus := 200 + respBody := "{\"bid\":false}" + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + requestStartTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + + bidRequest := adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), + } + + bidderAdapter := bidderAdapter{ + me: &metricsConfig.NilMetricsEngine{}, + Client: server.Client(), + } + logger := func(msg string, args ...interface{}) {} + + tests := []struct { + ctxDeadline time.Time + description string + tmaxAdjustments *TmaxAdjustmentsPreprocessed + assertFn func(err error) + }{ + { + ctxDeadline: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + description: "returns-tmax-timeout-error", + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000}, + assertFn: func(err error) { assert.Equal(t, errTmaxTimeout, err) }, + }, + { + ctxDeadline: time.Now().Add(5 * time.Second), + description: "remaining-duration-greater-than-tmax-min", + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 100}, + assertFn: func(err error) { assert.Nil(t, err) }, + }, + { + description: "tmax-disabled", + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: false}, + assertFn: func(err error) { assert.Nil(t, err) }, + }, + { + description: "tmax-BidderResponseDurationMin-not-set", + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 0}, + assertFn: func(err error) { assert.Nil(t, err) }, + }, + { + description: "tmax-is-nil", + tmaxAdjustments: nil, + assertFn: func(err error) { assert.Nil(t, err) }, + }, + } + for _, test := range tests { + var ( + ctx context.Context + cancelFn context.CancelFunc + ) + + if test.ctxDeadline.IsZero() { + ctx = context.Background() + } else { + ctx, cancelFn = context.WithDeadline(context.Background(), test.ctxDeadline) + defer cancelFn() + } + + httpCallInfo := bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, requestStartTime, test.tmaxAdjustments) + test.assertFn(httpCallInfo.err) + } +} + +func TestDoRequestImplWithTmaxTimeout(t *testing.T) { + respStatus := 200 + respBody := "{\"bid\":false}" + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + requestStartTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + + bidRequest := adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), + } + + metricsMock := &metrics.MetricsEngineMock{} + metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() + metricsMock.On("RecordTMaxTimeout").Once() + + bidderAdapter := bidderAdapter{ + me: metricsMock, + Client: server.Client(), + } + logger := func(msg string, args ...interface{}) {} + + tests := []struct { + ctxDeadline time.Time + description string + tmaxAdjustments *TmaxAdjustmentsPreprocessed + assertFn func(err error) + }{ + { + ctxDeadline: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + description: "returns-tmax-timeout-error", + tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000}, + assertFn: func(err error) { assert.Equal(t, errTmaxTimeout, err) }, + }, + } + for _, test := range tests { + var ( + ctx context.Context + cancelFn context.CancelFunc + ) + + if test.ctxDeadline.IsZero() { + ctx = context.Background() + } else { + ctx, cancelFn = context.WithDeadline(context.Background(), test.ctxDeadline) + defer cancelFn() + } + + httpCallInfo := bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, requestStartTime, test.tmaxAdjustments) + test.assertFn(httpCallInfo.err) + } } diff --git a/exchange/bidder_validate_bids.go b/exchange/bidder_validate_bids.go index 44d44a3503b..9b5771a3497 100644 --- a/exchange/bidder_validate_bids.go +++ b/exchange/bidder_validate_bids.go @@ -31,14 +31,14 @@ type validatedBidder struct { bidder AdaptedBidder } -func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) { - seatBids, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor) +func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { + seatBids, extraBidderRespInfo, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor, ruleToAdjustments) for _, seatBid := range seatBids { if validationErrors := removeInvalidBids(bidderRequest.BidRequest, seatBid); len(validationErrors) > 0 { errs = append(errs, validationErrors...) } } - return seatBids, errs + return seatBids, extraBidderRespInfo, errs } // validateBids will run some validation checks on the returned bids and excise any invalid bids diff --git a/exchange/bidder_validate_bids_test.go b/exchange/bidder_validate_bids_test.go index f863e93234a..ed6173b64ad 100644 --- a/exchange/bidder_validate_bids_test.go +++ b/exchange/bidder_validate_bids_test.go @@ -65,7 +65,7 @@ func TestAllValidBids(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + seatBids, _, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) assert.Len(t, seatBids, 1) assert.Len(t, seatBids[0].Bids, 4) assert.Len(t, errs, 0) @@ -135,7 +135,7 @@ func TestAllBadBids(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + seatBids, _, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) assert.Len(t, seatBids, 1) assert.Len(t, seatBids[0].Bids, 0) assert.Len(t, errs, 7) @@ -216,7 +216,7 @@ func TestMixedBids(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + seatBids, _, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) assert.Len(t, seatBids, 1) assert.Len(t, seatBids[0].Bids, 3) assert.Len(t, errs, 5) @@ -345,7 +345,7 @@ func TestCurrencyBids(t *testing.T) { addCallSignHeader: false, bidAdjustments: bidAdjustments, } - seatBids, errs := bidder.requestBid(context.Background(), bidderRequest, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}) + seatBids, _, errs := bidder.requestBid(context.Background(), bidderRequest, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) assert.Len(t, seatBids, 1) assert.Len(t, seatBids[0].Bids, expectedValidBids) assert.Len(t, errs, expectedErrs) @@ -354,9 +354,10 @@ func TestCurrencyBids(t *testing.T) { type mockAdaptedBidder struct { bidResponse []*entities.PbsOrtbSeatBid + extraRespInfo extraBidderRespInfo errorResponse []error } -func (b *mockAdaptedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) { - return b.bidResponse, b.errorResponse +func (b *mockAdaptedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { + return b.bidResponse, b.extraRespInfo, b.errorResponse } diff --git a/exchange/entities/entities.go b/exchange/entities/entities.go index 8a4121dcca8..1220da5c812 100644 --- a/exchange/entities/entities.go +++ b/exchange/entities/entities.go @@ -32,6 +32,7 @@ type PbsOrtbSeatBid struct { // PbsOrtbBid.BidTargets does not need to be filled out by the Bidder. It will be set later by the exchange. // PbsOrtbBid.BidVideo is optional but should be filled out by the Bidder if BidType is video. // PbsOrtbBid.BidEvents is set by exchange when event tracking is enabled +// PbsOrtbBid.BidFloors is set by exchange when floors is enabled // PbsOrtbBid.DealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns. // PbsOrtbBid.DealTierSatisfied is set to true by exchange.updateHbPbCatDur if deal tier satisfied otherwise it will be set to false // PbsOrtbBid.GeneratedBidID is unique Bid id generated by prebid server if generate Bid id option is enabled in config @@ -42,6 +43,7 @@ type PbsOrtbBid struct { BidTargets map[string]string BidVideo *openrtb_ext.ExtBidPrebidVideo BidEvents *openrtb_ext.ExtBidPrebidEvents + BidFloors *openrtb_ext.ExtBidPrebidFloors DealPriority int DealTierSatisfied bool GeneratedBidID string diff --git a/exchange/events.go b/exchange/events.go index 73ca6ec9788..fd52d6b676a 100644 --- a/exchange/events.go +++ b/exchange/events.go @@ -28,7 +28,7 @@ type eventTracking struct { func getEventTracking(requestExtPrebid *openrtb_ext.ExtRequestPrebid, ts time.Time, account *config.Account, bidderInfos config.BidderInfos, externalURL string) *eventTracking { return &eventTracking{ accountID: account.ID, - enabledForAccount: account.EventsEnabled, + enabledForAccount: account.Events.IsEnabled(), enabledForRequest: requestExtPrebid != nil && requestExtPrebid.Events != nil, auctionTimestampMs: ts.UnixNano() / 1e+6, integrationType: getIntegrationType(requestExtPrebid), @@ -60,7 +60,7 @@ func (ev *eventTracking) modifyBidsForEvents(seatBids map[openrtb_ext.BidderName // isModifyingVASTXMLAllowed returns true if this bidder config allows modifying VAST XML for event tracking func (ev *eventTracking) isModifyingVASTXMLAllowed(bidderName string) bool { - return ev.bidderInfos[bidderName].ModifyingVastXmlAllowed && (ev.enabledForAccount || ev.enabledForRequest) + return ev.bidderInfos[bidderName].ModifyingVastXmlAllowed && ev.isEventAllowed() } // modifyBidVAST injects event Impression url if needed, otherwise returns original VAST string @@ -81,7 +81,7 @@ func (ev *eventTracking) modifyBidVAST(pbsBid *entities.PbsOrtbBid, bidderName o // modifyBidJSON injects "wurl" (win) event url if needed, otherwise returns original json func (ev *eventTracking) modifyBidJSON(pbsBid *entities.PbsOrtbBid, bidderName openrtb_ext.BidderName, jsonBytes []byte) ([]byte, error) { - if !(ev.enabledForAccount || ev.enabledForRequest) || pbsBid.BidType == openrtb_ext.BidTypeVideo { + if !ev.isEventAllowed() || pbsBid.BidType == openrtb_ext.BidTypeVideo { return jsonBytes, nil } var winEventURL string @@ -104,7 +104,7 @@ func (ev *eventTracking) modifyBidJSON(pbsBid *entities.PbsOrtbBid, bidderName o // makeBidExtEvents make the data for bid.ext.prebid.events if needed, otherwise returns nil func (ev *eventTracking) makeBidExtEvents(pbsBid *entities.PbsOrtbBid, bidderName openrtb_ext.BidderName) *openrtb_ext.ExtBidPrebidEvents { - if !(ev.enabledForAccount || ev.enabledForRequest) || pbsBid.BidType == openrtb_ext.BidTypeVideo { + if !ev.isEventAllowed() || pbsBid.BidType == openrtb_ext.BidTypeVideo { return nil } return &openrtb_ext.ExtBidPrebidEvents{ @@ -129,3 +129,8 @@ func (ev *eventTracking) makeEventURL(evType analytics.EventType, pbsBid *entiti Integration: ev.integrationType, }) } + +// isEnabled checks if events are enabled by default or on account/request level +func (ev *eventTracking) isEventAllowed() bool { + return ev.enabledForAccount || ev.enabledForRequest +} diff --git a/exchange/events_test.go b/exchange/events_test.go index a808173e11c..24dedf1a6f1 100644 --- a/exchange/events_test.go +++ b/exchange/events_test.go @@ -121,7 +121,7 @@ func Test_eventsData_modifyBidJSON(t *testing.T) { }, { name: "banner: broken json expected to fail patching", - args: args{enabledForAccount: true, enabledForRequest: true, bidType: openrtb_ext.BidTypeBanner, generatedBidId: ""}, + args: args{enabledForAccount: false, enabledForRequest: true, bidType: openrtb_ext.BidTypeBanner, generatedBidId: ""}, jsonBytes: []byte(`broken json`), want: nil, }, @@ -153,3 +153,42 @@ func Test_eventsData_modifyBidJSON(t *testing.T) { }) } } + +func Test_isEventAllowed(t *testing.T) { + type args struct { + enabledForAccount bool + enabledForRequest bool + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "enabled for account", + args: args{enabledForAccount: true, enabledForRequest: false}, + want: true, + }, + { + name: "enabled for request", + args: args{enabledForAccount: false, enabledForRequest: true}, + want: true, + }, + { + name: "disabled for account and request", + args: args{enabledForAccount: false, enabledForRequest: false}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + evData := &eventTracking{ + enabledForAccount: tt.args.enabledForAccount, + enabledForRequest: tt.args.enabledForRequest, + } + isEventAllowed := evData.isEventAllowed() + assert.Equal(t, tt.want, isEventAllowed) + }) + } +} diff --git a/exchange/exchange.go b/exchange/exchange.go index fa0ea6afe3d..70d92c08fba 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -14,8 +14,11 @@ import ( "strings" "time" + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adservertargeting" + "github.com/prebid/prebid-server/bidadjustment" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/errortypes" @@ -25,6 +28,7 @@ import ( "github.com/prebid/prebid-server/floors" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/hooks/hookexecution" + "github.com/prebid/prebid-server/macros" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/prebid_cache_client" @@ -47,7 +51,7 @@ type extCacheInstructions struct { // Exchange runs Auctions. Implementations must be threadsafe, and will be shared across many goroutines. type Exchange interface { // HoldAuction executes an OpenRTB v2.5 Auction. - HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error) + HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*AuctionResponse, error) } // IdFetcher can find the user's ID for a specific Bidder. @@ -64,7 +68,6 @@ type exchange struct { cache prebid_cache_client.Client cacheTime time.Duration gdprPermsBuilder gdpr.PermissionsBuilder - tcf2ConfigBuilder gdpr.TCF2ConfigBuilder currencyConverter *currency.RateConverter externalURL string gdprDefaultValue gdpr.Signal @@ -76,6 +79,7 @@ type exchange struct { server config.Server bidValidationEnforcement config.Validations requestSplitter requestSplitter + macroReplacer macros.Replacer floor config.PriceFloors } @@ -90,10 +94,11 @@ type seatResponseExtra struct { } type bidResponseWrapper struct { - adapterSeatBids []*entities.PbsOrtbSeatBid - adapterExtra *seatResponseExtra - bidder openrtb_ext.BidderName - adapter openrtb_ext.BidderName + adapterSeatBids []*entities.PbsOrtbSeatBid + adapterExtra *seatResponseExtra + bidder openrtb_ext.BidderName + adapter openrtb_ext.BidderName + bidderResponseStartTime time.Time } type BidIDGenerator interface { @@ -124,7 +129,7 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool { return rand.Intn(100) < 50 } -func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer) Exchange { +func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer) Exchange { bidderToSyncerKey := map[string]string{} for bidder, syncer := range syncersByBidder { bidderToSyncerKey[bidder] = syncer.Key() @@ -145,7 +150,6 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid me: metricsEngine, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2CfgBuilder, hostSChainNode: cfg.HostSChainNode, bidderInfo: infos, } @@ -160,7 +164,6 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid currencyConverter: currencyConverter, externalURL: cfg.ExternalURL, gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2CfgBuilder, me: metricsEngine, gdprDefaultValue: gdprDefaultValue, privacyConfig: privacyConfig, @@ -170,6 +173,7 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid server: config.Server{ExternalUrl: cfg.ExternalURL, GvlID: cfg.GDPR.HostVendorID, DataCenter: cfg.DataCenter}, bidValidationEnforcement: cfg.Validations, requestSplitter: requestSplitter, + macroReplacer: macroReplacer, floor: cfg.PriceFloors, } } @@ -192,6 +196,8 @@ type AuctionRequest struct { Warnings []error GlobalPrivacyControlHeader string ImpExtInfoMap map[string]ImpExtInfo + TCF2Config gdpr.TCF2ConfigReader + Activities privacy.ActivityControl // LegacyLabels is included here for temporary compatibility with cleanOpenRTBRequests // in HoldAuction until we get to factoring it away. Do not use for anything new. @@ -200,11 +206,13 @@ type AuctionRequest struct { // map of imp id to stored response StoredAuctionResponses stored_responses.ImpsWithBidResponses // map of imp id to bidder to stored response - StoredBidResponses stored_responses.ImpBidderStoredResp - BidderImpReplaceImpID stored_responses.BidderImpReplaceImpID - PubID string - HookExecutor hookexecution.StageExecutor - QueryParams url.Values + StoredBidResponses stored_responses.ImpBidderStoredResp + BidderImpReplaceImpID stored_responses.BidderImpReplaceImpID + PubID string + HookExecutor hookexecution.StageExecutor + QueryParams url.Values + BidderResponseStartTime time.Time + TmaxAdjustments *TmaxAdjustmentsPreprocessed } // BidderRequest holds the bidder specific request and all other @@ -218,7 +226,11 @@ type BidderRequest struct { ImpReplaceImpId map[string]bool } -func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error) { +func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*AuctionResponse, error) { + if r == nil { + return nil, nil + } + var floorErrs []error err := r.HookExecutor.ExecuteProcessedAuctionStage(r.BidRequestWrapper) if err != nil { @@ -307,9 +319,18 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * Prebid: *requestExtPrebid, SChain: requestExt.GetSChain(), } - bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, r, requestExtLegacy, gdprDefaultValue) + bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprDefaultValue) errs = append(errs, floorErrs...) + mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments) + if err != nil { + if errortypes.ContainsFatalError([]error{err}) { + return nil, err + } + errs = append(errs, err) + } + bidAdjustmentRules := bidadjustment.BuildRules(mergedBidAdj) + e.me.RecordRequestPrivacy(privacyLabels) if len(r.StoredAuctionResponses) > 0 || len(r.StoredBidResponses) > 0 { @@ -321,13 +342,14 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids) defer cancel() - var adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid - var adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra - var fledge *openrtb_ext.Fledge - var anyBidsReturned bool - - // List of bidders we have requests for. - var liveAdapters []openrtb_ext.BidderName + var ( + adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra + fledge *openrtb_ext.Fledge + anyBidsReturned bool + // List of bidders we have requests for. + liveAdapters []openrtb_ext.BidderName + ) if len(r.StoredAuctionResponses) > 0 { adapterBids, fledge, liveAdapters, err = buildStoredAuctionResponse(r.StoredAuctionResponses) @@ -347,19 +369,39 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } else if r.Account.AlternateBidderCodes != nil { alternateBidderCodes = *r.Account.AlternateBidderCodes } - - adapterBids, adapterExtra, fledge, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor) + var extraRespInfo extraAuctionResponseInfo + adapterBids, adapterExtra, extraRespInfo = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime, bidAdjustmentRules, r.TmaxAdjustments) + fledge = extraRespInfo.fledge + anyBidsReturned = extraRespInfo.bidsFound + r.BidderResponseStartTime = extraRespInfo.bidderResponseStartTime } - var auc *auction - var cacheErrs []error - var bidResponseExt *openrtb_ext.ExtBidResponse + var ( + auc *auction + cacheErrs []error + bidResponseExt *openrtb_ext.ExtBidResponse + seatNonBids = nonBids{} + ) + if anyBidsReturned { + if e.floor.Enabled { + var rejectedBids []*entities.PbsOrtbSeatBid + var enforceErrs []error + + adapterBids, enforceErrs, rejectedBids = floors.Enforce(r.BidRequestWrapper, adapterBids, r.Account, conversions) + errs = append(errs, enforceErrs...) + for _, rejectedBid := range rejectedBids { + errs = append(errs, &errortypes.Warning{ + Message: fmt.Sprintf("%s bid id %s rejected - bid price %.4f %s is less than bid floor %.4f %s for imp %s", rejectedBid.Seat, rejectedBid.Bids[0].Bid.ID, rejectedBid.Bids[0].Bid.Price, rejectedBid.Currency, rejectedBid.Bids[0].BidFloors.FloorValue, rejectedBid.Bids[0].BidFloors.FloorCurrency, rejectedBid.Bids[0].Bid.ImpID), + WarningCode: errortypes.FloorBidRejectionWarningCode}) + } + } + var bidCategory map[string]string //If includebrandcategory is present in ext then CE feature is on. if requestExtPrebid.Targeting != nil && requestExtPrebid.Targeting.IncludeBrandCategory != nil { var rejections []string - bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &seatNonBids) if err != nil { return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) } @@ -390,14 +432,14 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * // A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys) auc = newAuction(adapterBids, len(r.BidRequestWrapper.Imp), targData.preferDeals) auc.validateAndUpdateMultiBid(adapterBids, targData.preferDeals, r.Account.DefaultBidLimit) - auc.setRoundedPrices(targData.priceGranularity) + auc.setRoundedPrices(*targData) if requestExtPrebid.SupportDeals { dealErrs := applyDealSupport(r.BidRequestWrapper.BidRequest, auc, bidCategory, multiBidMap) errs = append(errs, dealErrs...) } - bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) if debugLog.DebugEnabledOrOverridden { if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil { debugLog.Data.Response = string(bidRespExtBytes) @@ -414,9 +456,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap) } - bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) } else { - bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) if debugLog.DebugEnabledOrOverridden { @@ -448,13 +490,19 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations) // Build the response - bidResponse, err := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs) - + bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs) bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute) bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) + if err != nil { + return nil, err + } + bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBids) - return bidResponse, err + return &AuctionResponse{ + BidResponse: bidResponse, + ExtBidResponse: bidResponseExt, + }, nil } func buildMultiBidMap(prebid *openrtb_ext.ExtRequestPrebid) map[string]openrtb_ext.ExtMultiBid { @@ -609,16 +657,20 @@ func (e *exchange) getAllBids( headerDebugAllowed bool, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, experiment *openrtb_ext.Experiment, - hookExecutor hookexecution.StageExecutor) ( + hookExecutor hookexecution.StageExecutor, + pbsRequestStartTime time.Time, + bidAdjustmentRules map[string][]openrtb_ext.Adjustment, + tmaxAdjustments *TmaxAdjustmentsPreprocessed) ( map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, map[openrtb_ext.BidderName]*seatResponseExtra, - *openrtb_ext.Fledge, - bool) { + extraAuctionResponseInfo) { // Set up pointers to the bid results adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, len(bidderRequests)) adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests)) chBids := make(chan *bidResponseWrapper, len(bidderRequests)) - bidsFound := false + extraRespInfo := extraAuctionResponseInfo{} + + e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime)) for _, bidder := range bidderRequests { // Here we actually call the adapters and collect the bids. @@ -642,12 +694,15 @@ func (e *exchange) getAllBids( reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader bidReqOptions := bidRequestOptions{ - accountDebugAllowed: accountDebugAllowed, - headerDebugAllowed: headerDebugAllowed, - addCallSignHeader: isAdsCertEnabled(experiment, e.bidderInfo[string(bidderRequest.BidderName)]), - bidAdjustments: bidAdjustments, + accountDebugAllowed: accountDebugAllowed, + headerDebugAllowed: headerDebugAllowed, + addCallSignHeader: isAdsCertEnabled(experiment, e.bidderInfo[string(bidderRequest.BidderName)]), + bidAdjustments: bidAdjustments, + tmaxAdjustments: tmaxAdjustments, + bidderRequestStartTime: start, } - seatBids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor) + seatBids, extraBidderRespInfo, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor, bidAdjustmentRules) + brw.bidderResponseStartTime = extraBidderRespInfo.respProcessingStartTime // Add in time reporting elapsed := time.Since(start) @@ -658,9 +713,8 @@ func (e *exchange) getAllBids( if len(seatBids) != 0 { ae.HttpCalls = seatBids[0].HttpCalls } - // Timing statistics - e.me.RecordAdapterTime(bidderRequest.BidderLabels, time.Since(start)) + e.me.RecordAdapterTime(bidderRequest.BidderLabels, elapsed) bidderRequest.BidderLabels.AdapterBids = bidsToMetric(brw.adapterSeatBids) bidderRequest.BidderLabels.AdapterErrors = errorsToMetric(err) // Append any bid validation errors to the error list @@ -680,12 +734,13 @@ func (e *exchange) getAllBids( }, chBids) go bidderRunner(bidder, conversions) } - var fledge *openrtb_ext.Fledge // Wait for the bidders to do their thing for i := 0; i < len(bidderRequests); i++ { brw := <-chBids - + if !brw.bidderResponseStartTime.IsZero() { + extraRespInfo.bidderResponseStartTime = brw.bidderResponseStartTime + } //if bidder returned no bids back - remove bidder from further processing for _, seatBid := range brw.adapterSeatBids { if seatBid != nil { @@ -698,18 +753,18 @@ func (e *exchange) getAllBids( } } // collect fledgeAuctionConfigs separately from bids, as empty seatBids may be discarded - fledge = collectFledgeFromSeatBid(fledge, bidderName, brw.adapter, seatBid) + extraRespInfo.fledge = collectFledgeFromSeatBid(extraRespInfo.fledge, bidderName, brw.adapter, seatBid) } } //but we need to add all bidders data to adapterExtra to have metrics and other metadata adapterExtra[brw.bidder] = brw.adapterExtra - if !bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].Bids) > 0 { - bidsFound = true + if !extraRespInfo.bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].Bids) > 0 { + extraRespInfo.bidsFound = true } } - return adapterBids, adapterExtra, fledge, bidsFound + return adapterBids, adapterExtra, extraRespInfo } func collectFledgeFromSeatBid(fledge *openrtb_ext.Fledge, bidderName openrtb_ext.BidderName, adapterName openrtb_ext.BidderName, seatBid *entities.PbsOrtbSeatBid) *openrtb_ext.Fledge { @@ -822,9 +877,8 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester -func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error) (*openrtb2.BidResponse, error) { +func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error) *openrtb2.BidResponse { bidResponse := new(openrtb2.BidResponse) - var err error bidResponse.ID = bidRequest.ID if len(liveAdapters) == 0 { @@ -843,10 +897,9 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ bidResponse.Cur = adapterSeatBids.Currency } } - bidResponse.SeatBid = seatBids - return bidResponse, err + return bidResponse } func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, error) { @@ -859,7 +912,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e return buffer.Bytes(), err } -func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { +func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBids *nonBids) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { res := make(map[string]string) type bidDedupe struct { @@ -908,7 +961,7 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT bidID := bid.Bid.ID var duration int var category string - var pb string + var priceBucket string if bid.BidVideo != nil { duration = bid.BidVideo.Duration @@ -921,6 +974,7 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT //on receiving bids from adapters if no unique IAB category is returned or if no ad server category is returned discard the bid bidsToRemove = append(bidsToRemove, bidInd) rejections = updateRejections(rejections, bidID, "Bid did not contain a category") + seatNonBids.addBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) continue } if translateCategories { @@ -942,35 +996,22 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT // TODO: consider should we remove bids with zero duration here? - pb = GetPriceBucket(bid.Bid.Price, targData.priceGranularity) + priceBucket = GetPriceBucket(*bid.Bid, *targData) - newDur := duration - if len(targeting.DurationRangeSec) > 0 { - durationRange := make([]int, len(targeting.DurationRangeSec)) - copy(durationRange, targeting.DurationRangeSec) - sort.Ints(durationRange) - - //if the bid is above the range of the listed durations (and outside the buffer), reject the bid - if duration > durationRange[len(durationRange)-1] { - bidsToRemove = append(bidsToRemove, bidInd) - rejections = updateRejections(rejections, bidID, "Bid duration exceeds maximum allowed") - continue - } - for _, dur := range durationRange { - if duration <= dur { - newDur = dur - break - } - } + newDur, err := findDurationRange(duration, targeting.DurationRangeSec) + if err != nil { + bidsToRemove = append(bidsToRemove, bidInd) + rejections = updateRejections(rejections, bidID, err.Error()) + continue } var categoryDuration string var dupeKey string if brandCatExt.WithCategory { - categoryDuration = fmt.Sprintf("%s_%s_%ds", pb, category, newDur) + categoryDuration = fmt.Sprintf("%s_%s_%ds", priceBucket, category, newDur) dupeKey = category } else { - categoryDuration = fmt.Sprintf("%s_%ds", pb, newDur) + categoryDuration = fmt.Sprintf("%s_%ds", priceBucket, newDur) dupeKey = categoryDuration } @@ -984,7 +1025,7 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT if err != nil { dupeBidPrice = 0 } - currBidPrice, err := strconv.ParseFloat(pb, 64) + currBidPrice, err := strconv.ParseFloat(priceBucket, 64) if err != nil { currBidPrice = 0 } @@ -1025,7 +1066,7 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT } } res[bidID] = categoryDuration - dedupe[dupeKey] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID, bidPrice: pb} + dedupe[dupeKey] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID, bidPrice: priceBucket} } if len(bidsToRemove) > 0 { @@ -1051,6 +1092,33 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT return res, seatBids, rejections, nil } +// findDurationRange returns the element in the array 'durationRanges' that is both greater than 'dur' and closest +// in value to 'dur' unless a value equal to 'dur' is found. Returns an error if all elements in 'durationRanges' +// are less than 'dur'. +func findDurationRange(dur int, durationRanges []int) (int, error) { + newDur := dur + madeSelection := false + var err error + + for i := range durationRanges { + if dur > durationRanges[i] { + continue + } + if dur == durationRanges[i] { + return durationRanges[i], nil + } + // dur < durationRanges[i] + if durationRanges[i] < newDur || !madeSelection { + newDur = durationRanges[i] + madeSelection = true + } + } + if !madeSelection && len(durationRanges) > 0 { + err = errors.New("bid duration exceeds maximum allowed") + } + return newDur, err +} + func removeBidById(seatBid *entities.PbsOrtbSeatBid, bidID string) { //Find index of bid to remove dupeBidIndex := -1 @@ -1129,16 +1197,20 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*en } if len(errList) > 0 { bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = errsToBidderErrors(errList) + if prebidWarn := errsToBidderWarnings(errList); len(prebidWarn) > 0 { + bidResponseExt.Warnings[openrtb_ext.PrebidExtKey] = prebidWarn + } } bidResponseExt.ResponseTimeMillis[bidderName] = responseExtra.ResponseTimeMillis // Defering the filling of bidResponseExt.Usersync[bidderName] until later } + return bidResponseExt } // Return an openrtb seatBid for a bidder -// BuildBidResponse is responsible for ensuring nil bid seatbids are not included +// buildBidResponse is responsible for ensuring nil bid seatbids are not included func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string) *openrtb2.SeatBid { seatBid := &openrtb2.SeatBid{ Seat: adapter.String(), @@ -1181,6 +1253,7 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea DealTierSatisfied: bid.DealTierSatisfied, Events: bid.BidEvents, Targeting: bid.BidTargets, + Floors: bid.BidFloors, Type: bid.BidType, Meta: bid.BidMeta, Video: bid.BidVideo, @@ -1349,20 +1422,6 @@ func listBiddersWithRequests(bidderRequests []BidderRequest) []openrtb_ext.Bidde return liveAdapters } -func getPrebidMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { - if bid.Ext != nil { - var bidExt openrtb_ext.ExtBid - err := json.Unmarshal(bid.Ext, &bidExt) - if err == nil && bidExt.Prebid != nil { - return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) - } - } - - return "", &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Failed to parse bid mediatype for impression \"%s\"", bid.ImpID), - } -} - func buildStoredAuctionResponse(storedAuctionResponses map[string]json.RawMessage) ( map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, *openrtb_ext.Fledge, @@ -1383,29 +1442,9 @@ func buildStoredAuctionResponse(storedAuctionResponses map[string]json.RawMessag //set imp id from request for i := range seat.Bid { seat.Bid[i].ImpID = impId - mType := seat.Bid[i].MType - var bidType openrtb_ext.BidType - if mType > 0 { - switch mType { - case openrtb2.MarkupBanner: - bidType = openrtb_ext.BidTypeBanner - case openrtb2.MarkupVideo: - bidType = openrtb_ext.BidTypeVideo - case openrtb2.MarkupAudio: - bidType = openrtb_ext.BidTypeAudio - case openrtb2.MarkupNative: - bidType = openrtb_ext.BidTypeNative - default: - return nil, nil, nil, &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Failed to parse bid mType for impression \"%s\"", seat.Bid[i].ImpID), - } - } - } else { - var err error - bidType, err = getPrebidMediaTypeForBid(seat.Bid[i]) - if err != nil { - return nil, nil, nil, err - } + bidType, err := getMediaTypeForBid(seat.Bid[i]) + if err != nil { + return nil, nil, nil, err } bidsToAdd = append(bidsToAdd, &entities.PbsOrtbBid{Bid: &seat.Bid[i], BidType: bidType}) } @@ -1521,3 +1560,19 @@ func setErrorMessageSecureMarkup(validationType string) string { } return "" } + +// setSeatNonBid adds SeatNonBids within bidResponse.Ext.Prebid.SeatNonBid +func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBids nonBids) *openrtb_ext.ExtBidResponse { + if len(seatNonBids.seatNonBidsMap) == 0 { + return bidResponseExt + } + if bidResponseExt == nil { + bidResponseExt = &openrtb_ext.ExtBidResponse{} + } + if bidResponseExt.Prebid == nil { + bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{} + } + + bidResponseExt.Prebid.SeatNonBid = seatNonBids.get() + return bidResponseExt +} diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 3b2abb21508..919f8dceb54 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "math" "net/http" "net/http/httptest" "os" @@ -30,11 +31,13 @@ import ( "github.com/prebid/prebid-server/hooks" "github.com/prebid/prebid-server/hooks/hookexecution" "github.com/prebid/prebid-server/hooks/hookstage" + "github.com/prebid/prebid-server/macros" "github.com/prebid/prebid-server/metrics" metricsConf "github.com/prebid/prebid-server/metrics/config" metricsConfig "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" pbc "github.com/prebid/prebid-server/prebid_cache_client" + "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/file_fetcher" "github.com/prebid/prebid-server/usersync" @@ -78,11 +81,8 @@ func TestNewExchange(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) for _, bidderName := range knownAdapters { if _, ok := e.adapterMap[bidderName]; !ok { if biddersInfo[string(bidderName)].IsEnabled() { @@ -131,11 +131,8 @@ func TestCharacterEscape(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs //liveAdapters []openrtb_ext.BidderName, @@ -171,12 +168,9 @@ func TestCharacterEscape(t *testing.T) { var errList []error // 4) Build bid response - bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList) + bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList) // 5) Assert we have no errors and one '&' character as we are supposed to - if err != nil { - t.Errorf("exchange.buildBidResponse returned unexpected error: %v", err) - } if len(errList) > 0 { t.Errorf("exchange.buildBidResponse returned %d errors", len(errList)) } @@ -335,15 +329,11 @@ func TestDebugBehaviour(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher e.requestSplitter = requestSplitter{ - me: &metricsConf.NilMetricsEngine{}, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: &metricsConf.NilMetricsEngine{}, + gdprPermsBuilder: e.gdprPermsBuilder, } ctx := context.Background() @@ -362,12 +352,13 @@ func TestDebugBehaviour(t *testing.T) { bidRequest.Ext = nil } - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, Account: config.Account{DebugAllow: test.debugData.accountLevelDebugAllowed}, UserSyncs: &emptyUsersync{}, StartTime: time.Now(), HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } if test.generateWarnings { var errL []error @@ -386,7 +377,7 @@ func TestDebugBehaviour(t *testing.T) { // Assert no HoldAuction error assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err) assert.NotNilf(t, outBidResponse.Ext, "%s. outBidResponse.Ext should not be nil \n", test.desc) - + assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) actualExt := &openrtb_ext.ExtBidResponse{} err = json.Unmarshal(outBidResponse.Ext, actualExt) assert.NoErrorf(t, err, "%s. \"ext\" JSON field could not be unmarshaled. err: \"%v\" \n outBidResponse.Ext: \"%s\" \n", test.desc, err, outBidResponse.Ext) @@ -504,15 +495,11 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } debugLog := DebugLog{Enabled: true} @@ -533,12 +520,13 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) { bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`) - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, Account: config.Account{DebugAllow: true}, UserSyncs: &emptyUsersync{}, StartTime: time.Now(), HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ @@ -550,6 +538,7 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) { // Assert no HoldAuction err assert.NoErrorf(t, err, "ex.HoldAuction returned an err") assert.NotNilf(t, outBidResponse.Ext, "outBidResponse.Ext should not be nil") + assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) actualExt := &openrtb_ext.ExtBidResponse{} err = json.Unmarshal(outBidResponse.Ext, actualExt) @@ -671,16 +660,12 @@ func TestOverrideWithCustomCurrency(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.currencyConverter = mockCurrencyConverter e.categoriesFetcher = categoriesFetcher e.bidIDGenerator = &mockBidIDGenerator{false, false} e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } // Define mock incoming bid requeset @@ -716,11 +701,12 @@ func TestOverrideWithCustomCurrency(t *testing.T) { // Set bidRequest currency list mockBidRequest.Cur = []string{test.in.bidRequestCurrency} - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } // Run test @@ -728,6 +714,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) { // Assertions assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) + assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) if test.expected.numBids > 0 { // Assert out currency @@ -778,9 +765,6 @@ func TestAdapterCurrency(t *testing.T) { allowAllBidders: true, }, }.Builder, - tcf2ConfigBuilder: fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder, currencyConverter: currencyConverter, categoriesFetcher: nilCategoryFetcher{}, bidIDGenerator: &mockBidIDGenerator{false, false}, @@ -789,9 +773,8 @@ func TestAdapterCurrency(t *testing.T) { }, } e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } // Define Bid Request @@ -811,11 +794,12 @@ func TestAdapterCurrency(t *testing.T) { } // Run Auction - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } response, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) assert.NoError(t, err) @@ -852,18 +836,14 @@ func TestFloorsSignalling(t *testing.T) { allowAllBidders: true, }, }.Builder, - tcf2ConfigBuilder: fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder, currencyConverter: currencyConverter, categoriesFetcher: nilCategoryFetcher{}, bidIDGenerator: &mockBidIDGenerator{false, false}, floor: config.PriceFloors{Enabled: true}, } e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } type testResults struct { @@ -984,7 +964,7 @@ func TestFloorsSignalling(t *testing.T) { } for _, test := range testCases { - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: test.req, Account: config.Account{DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: test.floorsEnable, MaxRule: 100, MaxSchemaDims: 5}}, UserSyncs: &emptyUsersync{}, @@ -1340,16 +1320,12 @@ func TestReturnCreativeEndToEnd(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher e.bidIDGenerator = &mockBidIDGenerator{false, false} e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } // Define mock incoming bid requeset @@ -1368,11 +1344,12 @@ func TestReturnCreativeEndToEnd(t *testing.T) { for _, test := range testGroup.testCases { mockBidRequest.Ext = test.inExt - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } // Run test @@ -1385,6 +1362,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) { continue } else { assert.NoErrorf(t, err, "%s: %s. HoldAuction error: %v \n", testGroup.groupDesc, test.desc, err) + assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) } // Assert returned bid @@ -1448,11 +1426,8 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs liveAdapters := []openrtb_ext.BidderName{bidderName} @@ -1556,10 +1531,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { var errList []error // 4) Build bid response - bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList) - - // 5) Assert we have no errors and the bid response we expected - assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error") + bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList) expectedBidResponse := &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ @@ -1811,11 +1783,8 @@ func TestBidResponseCurrency(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -1935,8 +1904,7 @@ func TestBidResponseCurrency(t *testing.T) { } // Run tests for i := range testCases { - actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList) - assert.NoError(t, err, fmt.Sprintf("[TEST_FAILED] e.buildBidResponse resturns error in test: %s Error message: %s \n", testCases[i].description, err)) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList) assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext)) } } @@ -1950,9 +1918,6 @@ func TestBidResponseImpExtInfo(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } server := httptest.NewServer(http.HandlerFunc(noBidHandler)) @@ -1965,7 +1930,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { t.Fatalf("Error intializing adapters: %v", adaptersErr) } - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, tcf2ConfigBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -2005,8 +1970,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { expectedBidResponseExt := `{"origbidcpm":0,"prebid":{"type":"video","passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}` - actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList) - assert.NoError(t, err, fmt.Sprintf("imp ext info was not passed through correctly: %s", err)) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList) resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext) assert.Equalf(t, expectedBidResponseExt, resBidExt, "Expected bid response extension is incorrect") @@ -2042,11 +2006,12 @@ func TestRaceIntegration(t *testing.T) { currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } debugLog := DebugLog{} @@ -2056,11 +2021,8 @@ func TestRaceIntegration(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2CfgBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2CfgBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) _, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) @@ -2119,7 +2081,7 @@ func getTestBuildRequest(t *testing.T) *openrtb2.BidRequest { H: 600, }}, }, - Ext: json.RawMessage(`{"ext_field":"value}"}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), }, { Video: &openrtb2.Video{ MIMEs: []string{"video/mp4"}, @@ -2128,7 +2090,7 @@ func getTestBuildRequest(t *testing.T) *openrtb2.BidRequest { W: 300, H: 600, }, - Ext: json.RawMessage(`{"ext_field":"value}"}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), }}, } } @@ -2157,11 +2119,8 @@ func TestPanicRecovery(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) chBids := make(chan *bidResponseWrapper, 1) panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) { @@ -2231,10 +2190,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}).(*exchange) + e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange) e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{} e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{} @@ -2267,11 +2223,12 @@ func TestPanicRecoveryHighLevel(t *testing.T) { }}, } - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } debugLog := DebugLog{} _, err = e.HoldAuction(context.Background(), auctionRequest, &debugLog) @@ -2368,7 +2325,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { if spec.BidIDGenerator != nil { *bidIdGenerator = *spec.BidIDGenerator } - ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.HostConfigBidValidation, spec.Server) + ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.FloorsEnabled, spec.HostConfigBidValidation, spec.Server) biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest) debugLog := &DebugLog{} if spec.DebugLog != nil { @@ -2395,17 +2352,27 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { impExtInfoMap[impID] = ImpExtInfo{} } - auctionRequest := AuctionRequest{ + activityControl, err := privacy.NewActivityControl(spec.AccountPrivacy) + if err != nil { + t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error()) + } + + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest}, Account: config.Account{ - ID: "testaccount", - EventsEnabled: spec.EventsEnabled, - DebugAllow: true, - Validations: spec.AccountConfigBidValidation, + ID: "testaccount", + Events: config.Events{ + Enabled: &spec.EventsEnabled, + }, + DebugAllow: true, + PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled}, + Validations: spec.AccountConfigBidValidation, }, UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs), ImpExtInfoMap: impExtInfoMap, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, config.AccountGDPR{}), + Activities: activityControl, } if spec.MultiBid != nil { @@ -2436,7 +2403,11 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { } ctx := context.Background() - bid, err := ex.HoldAuction(ctx, auctionRequest, debugLog) + aucResponse, err := ex.HoldAuction(ctx, auctionRequest, debugLog) + var bid *openrtb2.BidResponse + if aucResponse != nil { + bid = aucResponse.BidResponse + } if len(spec.Response.Error) > 0 && spec.Response.Bids == nil { if err.Error() != spec.Response.Error { t.Errorf("%s: Exchange returned different errors. Expected %s, got %s", filename, spec.Response.Error, err.Error()) @@ -2571,7 +2542,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb2.BidRespons } } -func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag bool, hostBidValidation config.Validations, server exchangeServer) Exchange { +func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag, floorsFlag bool, hostBidValidation config.Validations, server exchangeServer) Exchange { bidderAdapters := make(map[openrtb_ext.BidderName]AdaptedBidder, len(expectations)) bidderInfos := make(config.BidderInfos, len(expectations)) for _, bidderName := range openrtb_ext.CoreBidderNames() { @@ -2614,9 +2585,6 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, config.AccountGDPR{}), - }.Builder bidderToSyncerKey := map[string]string{} for _, bidderName := range openrtb_ext.CoreBidderNames() { @@ -2641,7 +2609,6 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] me: metricsEngine, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: hostSChainNode, bidderInfo: bidderInfos, } @@ -2654,7 +2621,6 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), gdprDefaultValue: gdprDefaultValue, gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, privacyConfig: privacyConfig, categoriesFetcher: categoriesFetcher, bidderInfo: bidderInfos, @@ -2665,6 +2631,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] server: config.Server{ExternalUrl: server.ExternalUrl, GvlID: server.GvlID, DataCenter: server.DataCenter}, bidValidationEnforcement: hostBidValidation, requestSplitter: requestSplitter, + floor: config.PriceFloors{Enabled: floorsFlag}, } } @@ -2777,10 +2744,10 @@ func TestCategoryMapping(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 40.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""} + bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 40.0000, "USD", ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2794,7 +2761,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2832,10 +2799,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 40.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""} + bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 40.0000, "USD", ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2849,7 +2816,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2886,9 +2853,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2901,7 +2868,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2968,9 +2935,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2983,7 +2950,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -3020,11 +2987,11 @@ func TestCategoryDedupe(t *testing.T) { bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 15.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 15.0000, "USD", ""} + bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -3053,7 +3020,7 @@ func TestCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 3, len(rejections), "There should be 2 bid rejection messages") @@ -3099,11 +3066,11 @@ func TestNoCategoryDedupe(t *testing.T) { bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""} + bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -3133,7 +3100,7 @@ func TestNoCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") @@ -3179,8 +3146,8 @@ func TestCategoryMappingBidderName(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} innerBids1 := []*entities.PbsOrtbBid{ &bid1_1, @@ -3198,7 +3165,7 @@ func TestCategoryMappingBidderName(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3224,7 +3191,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { includeWinners: true, } - requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} + requestExt.Prebid.Targeting.DurationRangeSec = []int{30, 10, 25, 5, 20, 50} adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) @@ -3233,8 +3200,8 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 12.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 17}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 8}, nil, nil, 0, false, "", 12.0000, "USD", ""} innerBids1 := []*entities.PbsOrtbBid{ &bid1_1, @@ -3252,12 +3219,12 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") - assert.Equal(t, "10.00_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") - assert.Equal(t, "12.00_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") + assert.Equal(t, "10.00_20s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") + assert.Equal(t, "12.00_10s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") assert.Len(t, adapterBids[bidderName1].Bids, 1, "Bidders number doesn't match") assert.Len(t, adapterBids[bidderName2].Bids, 1, "Bidders number doesn't match") assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") @@ -3323,7 +3290,7 @@ func TestBidRejectionErrors(t *testing.T) { }, duration: 70, expectedRejections: []string{ - "bid rejected [bid ID: bid_id1] reason: Bid duration exceeds maximum allowed", + "bid rejected [bid ID: bid_id1] reason: bid duration exceeds maximum allowed", }, }, { @@ -3345,7 +3312,7 @@ func TestBidRejectionErrors(t *testing.T) { innerBids := []*entities.PbsOrtbBid{} for _, bid := range test.bids { currentBid := entities.PbsOrtbBid{ - bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, "", 10.0000, "USD", ""} + bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, nil, 0, false, "", 10.0000, "USD", ""} innerBids = append(innerBids, ¤tBid) } @@ -3353,7 +3320,7 @@ func TestBidRejectionErrors(t *testing.T) { adapterBids[bidderName] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) if len(test.expectedCatDur) > 0 { // Bid deduplication case @@ -3393,8 +3360,8 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} innerBidsApn1 := []*entities.PbsOrtbBid{ &bid1_Apn1, @@ -3416,7 +3383,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - bidCategory, _, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, _, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Len(t, rejections, 1, "There should be 1 bid rejection message") @@ -3473,11 +3440,11 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1} bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} innerBidsApn1 := []*entities.PbsOrtbBid{ &bid1_Apn1_1, @@ -3500,7 +3467,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - _, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true}) + _, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true}, &nonBids{}) assert.NoError(t, err, "Category mapping error should be empty") @@ -3555,9 +3522,9 @@ func TestRemoveBidById(t *testing.T) { bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1} bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""} - bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} type aTest struct { desc string @@ -3627,7 +3594,7 @@ func TestApplyDealSupport(t *testing.T) { { description: "hb_pb_cat_dur should be modified", dealPriority: 5, - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), targ: map[string]string{ "hb_pb_cat_dur": "12.00_movies_30s", }, @@ -3638,7 +3605,7 @@ func TestApplyDealSupport(t *testing.T) { { description: "hb_pb_cat_dur should not be modified due to priority not exceeding min", dealPriority: 9, - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}`), + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}}}`), targ: map[string]string{ "hb_pb_cat_dur": "12.00_medicine_30s", }, @@ -3649,7 +3616,7 @@ func TestApplyDealSupport(t *testing.T) { { description: "hb_pb_cat_dur should not be modified due to invalid config", dealPriority: 5, - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}`), + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}}}`), targ: map[string]string{ "hb_pb_cat_dur": "12.00_games_30s", }, @@ -3660,7 +3627,7 @@ func TestApplyDealSupport(t *testing.T) { { description: "hb_pb_cat_dur should not be modified due to deal priority of 0", dealPriority: 0, - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), targ: map[string]string{ "hb_pb_cat_dur": "12.00_auto_30s", }, @@ -3682,7 +3649,7 @@ func TestApplyDealSupport(t *testing.T) { }, } - bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", ""} + bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""} bidCategory := map[string]string{ bid.Bid.ID: test.targ["hb_pb_cat_dur"], } @@ -3730,11 +3697,11 @@ func TestApplyDealSupportMultiBid(t *testing.T) { Imp: []openrtb2.Imp{ { ID: "imp_id1", - Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), }, { ID: "imp_id1", - Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), }, }, }, @@ -3742,8 +3709,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, }, }, }, @@ -3776,11 +3743,11 @@ func TestApplyDealSupportMultiBid(t *testing.T) { Imp: []openrtb2.Imp{ { ID: "imp_id1", - Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), }, { ID: "imp_id1", - Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), }, }, }, @@ -3788,8 +3755,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, }, }, }, @@ -3827,11 +3794,11 @@ func TestApplyDealSupportMultiBid(t *testing.T) { Imp: []openrtb2.Imp{ { ID: "imp_id1", - Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), }, { ID: "imp_id1", - Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), }, }, }, @@ -3839,8 +3806,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, }, }, }, @@ -3904,7 +3871,7 @@ func TestGetDealTiers(t *testing.T) { description: "One", request: openrtb2.BidRequest{ Imp: []openrtb2.Imp{ - {ID: "imp1", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}}}`)}, + {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}}}}}`)}, }, }, expected: map[string]openrtb_ext.DealTierBidderMap{ @@ -3915,8 +3882,8 @@ func TestGetDealTiers(t *testing.T) { description: "Many", request: openrtb2.BidRequest{ Imp: []openrtb2.Imp{ - {ID: "imp1", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}`)}, - {ID: "imp2", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 8, "prefix": "tier2"}}}`)}, + {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}}}`)}, + {ID: "imp2", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 8, "prefix": "tier2"}}}}}`)}, }, }, expected: map[string]openrtb_ext.DealTierBidderMap{ @@ -3928,8 +3895,8 @@ func TestGetDealTiers(t *testing.T) { description: "Many - Skips Malformed", request: openrtb2.BidRequest{ Imp: []openrtb2.Imp{ - {ID: "imp1", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}`)}, - {ID: "imp2", Ext: json.RawMessage(`{"appnexus": {"dealTier": "wrong type"}}`)}, + {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}}}`)}, + {ID: "imp2", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": "wrong type"}}}}`)}, }, }, expected: map[string]openrtb_ext.DealTierBidderMap{ @@ -4031,7 +3998,7 @@ func TestUpdateHbPbCatDur(t *testing.T) { } for _, test := range testCases { - bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", ""} + bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""} bidCategory := map[string]string{ bid.Bid.ID: test.targ["hb_pb_cat_dur"], } @@ -4262,9 +4229,6 @@ func TestStoredAuctionResponses(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder // Define mock incoming bid requeset mockBidRequest := &openrtb2.BidRequest{ @@ -4310,12 +4274,13 @@ func TestStoredAuctionResponses(t *testing.T) { for _, test := range testCases { - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, StoredAuctionResponses: test.storedAuctionResp, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } // Run test outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) @@ -4324,7 +4289,7 @@ func TestStoredAuctionResponses(t *testing.T) { } else { assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) outBidResponse.Ext = nil - assert.Equal(t, expectedBidResponse, outBidResponse, "Incorrect stored auction response") + assert.Equal(t, expectedBidResponse, outBidResponse.BidResponse, "Incorrect stored auction response") } } @@ -4388,7 +4353,7 @@ func TestBuildStoredAuctionResponses(t *testing.T) { }, liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, }, - errorMessage: "invalid BidType: incorrect", + errorMessage: "Failed to parse bid mediatype for impression \"impression-id\", invalid BidType: incorrect", }, { desc: "Single imp with multiple bids in stored response one bidder", @@ -4630,13 +4595,9 @@ func TestAuctionDebugEnabled(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } ctx := context.Background() @@ -4646,13 +4607,14 @@ func TestAuctionDebugEnabled(t *testing.T) { Test: 1, } - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, Account: config.Account{DebugAllow: false}, UserSyncs: &emptyUsersync{}, StartTime: time.Now(), RequestType: metrics.ReqTypeORTB2Web, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } debugLog := &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true} @@ -4702,11 +4664,8 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &signer).(*exchange) + e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer()).(*exchange) // Define mock incoming bid requeset mockBidRequest := &openrtb2.BidRequest{ @@ -4720,11 +4679,12 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) { Ext: json.RawMessage(`{"prebid":{"experiment":{"adscert":{"enabled": true}}}}`), } - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } debugLog := DebugLog{} @@ -5144,16 +5104,12 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) { allowAllBidders: true, }, }.Builder - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher e.bidIDGenerator = &mockBidIDGenerator{false, false} e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } // Define mock incoming bid requeset @@ -5175,11 +5131,12 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) { mockBidRequest.Ext = test.in.requestExt - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, Account: test.in.config.AccountDefaults, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } // Run test @@ -5188,6 +5145,7 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) { // Assertions assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) assert.NotNil(t, outBidResponse) + assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) // So 2 seatBids are expected as, // the default "" and "pubmatic" bids will be in one seat and the extra-bids "groupm"/"appnexus"/"ix" in another seat. @@ -5216,6 +5174,7 @@ func (ms *MockSigner) Sign(destinationURL string, body []byte) (string, error) { type exchangeSpec struct { GDPREnabled bool `json:"gdpr_enabled"` + FloorsEnabled bool `json:"floors_enabled"` IncomingRequest exchangeRequest `json:"incomingRequest"` OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` Response exchangeResponse `json:"response,omitempty"` @@ -5231,9 +5190,11 @@ type exchangeSpec struct { HostSChainFlag bool `json:"host_schain_flag,omitempty"` HostConfigBidValidation config.Validations `json:"host_bid_validations"` AccountConfigBidValidation config.Validations `json:"account_bid_validations"` + AccountFloorsEnabled bool `json:"account_floors_enabled"` FledgeEnabled bool `json:"fledge_enabled,omitempty"` MultiBid *multiBidSpec `json:"multiBid,omitempty"` Server exchangeServer `json:"server,omitempty"` + AccountPrivacy *config.AccountPrivacy `json:"accountPrivacy,omitempty"` } type multiBidSpec struct { @@ -5281,6 +5242,7 @@ type bidderResponse struct { type bidderSeatBid struct { Bids []bidderBid `json:"pbsBids,omitempty"` Seat string `json:"seat"` + Currency string `json:"currency"` FledgeAuctionConfigs []*openrtb_ext.FledgeAuctionConfig `json:"fledgeAuctionConfigs,omitempty"` } @@ -5313,7 +5275,7 @@ type validatingBidder struct { mockResponses map[string]bidderResponse } -func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) (seatBids []*entities.PbsOrtbSeatBid, errs []error) { +func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBids []*entities.PbsOrtbSeatBid, extaInfo extraBidderRespInfo, errs []error) { if expectedRequest, ok := b.expectations[string(bidderRequest.BidderName)]; ok { if expectedRequest != nil { if !reflect.DeepEqual(expectedRequest.BidAdjustments, bidRequestOptions.bidAdjustments) { @@ -5346,6 +5308,7 @@ func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderR Bids: bids, HttpCalls: mockResponse.HttpCalls, Seat: mockSeatBid.Seat, + Currency: mockSeatBid.Currency, FledgeAuctionConfigs: mockSeatBid.FledgeAuctionConfigs, }) } @@ -5371,7 +5334,7 @@ type capturingRequestBidder struct { req *openrtb2.BidRequest } -func (b *capturingRequestBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) (seatBid []*entities.PbsOrtbSeatBid, errs []error) { +func (b *capturingRequestBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBid []*entities.PbsOrtbSeatBid, errs []error) { b.req = bidderRequest.BidRequest return []*entities.PbsOrtbSeatBid{{}}, nil } @@ -5478,7 +5441,7 @@ func (e *emptyUsersync) HasAnyLiveSyncs() bool { type panicingAdapter struct{} -func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) (posb []*entities.PbsOrtbSeatBid, errs []error) { +func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (posb []*entities.PbsOrtbSeatBid, extraInfo extraBidderRespInfo, errs []error) { panic("Panic! Panic! The world is ending!") } @@ -5568,14 +5531,10 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) { e := new(exchange) e.me = &metricsConf.NilMetricsEngine{} - e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.requestSplitter = requestSplitter{ - me: e.me, - gdprPermsBuilder: e.gdprPermsBuilder, - tcf2ConfigBuilder: e.tcf2ConfigBuilder, + me: e.me, + gdprPermsBuilder: e.gdprPermsBuilder, } bidRequest := &openrtb2.BidRequest{ @@ -5595,12 +5554,13 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) { exec := hookexecution.NewHookExecutor(TestApplyHookMutationsBuilder{}, "/openrtb2/auction", &metricsConfig.NilMetricsEngine{}) - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, Account: config.Account{DebugAllow: true}, UserSyncs: &emptyUsersync{}, StartTime: time.Now(), HookExecutor: exec, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ @@ -5613,6 +5573,7 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) { _, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) // Assert no HoldAuction err assert.NoErrorf(t, err, "ex.HoldAuction returned an err") + assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) // check stage outcomes assert.Equal(t, len(exec.GetOutcomes()), len(e.adapterMap), "stage outcomes append operation failed") @@ -5678,3 +5639,117 @@ func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, mct return hookstage.HookResult[hookstage.BidderRequestPayload]{ChangeSet: c, ModuleContext: mctx.ModuleContext}, nil } + +func TestNilAuctionRequest(t *testing.T) { + ex := &exchange{} + response, err := ex.HoldAuction(context.Background(), nil, &DebugLog{}) + assert.Nil(t, response) + assert.Nil(t, err) +} + +func TestSelectNewDuration(t *testing.T) { + type testInput struct { + dur int + durRanges []int + } + type testOutput struct { + dur int + err error + } + testCases := []struct { + desc string + in testInput + expected testOutput + }{ + { + desc: "nil duration range array, don't expect error", + in: testInput{ + dur: 1, + durRanges: nil, + }, + expected: testOutput{1, nil}, + }, + { + desc: "empty duration range array, don't expect error", + in: testInput{ + dur: 1, + durRanges: []int{}, + }, + expected: testOutput{1, nil}, + }, + { + desc: "all duration range array elements less than duration, expect error", + in: testInput{ + dur: 5, + durRanges: []int{-1, 0, 1, 2, 3}, + }, + expected: testOutput{5, errors.New("bid duration exceeds maximum allowed")}, + }, + { + desc: "all duration range array elements greater than duration, expect smallest element in durRanges and nil error", + in: testInput{ + dur: 5, + durRanges: []int{9, math.MaxInt32, 8}, + }, + expected: testOutput{8, nil}, + }, + { + desc: "some array elements greater than duration, expect the value greater than dur that is closest in value.", + in: testInput{ + dur: 5, + durRanges: []int{math.MaxInt32, -3, 7, 2}, + }, + expected: testOutput{7, nil}, + }, + { + desc: "an entry in the duration range array is equal to duration, expect its value in return.", + in: testInput{ + dur: 5, + durRanges: []int{-3, math.MaxInt32, 5, 7}, + }, + expected: testOutput{5, nil}, + }, + } + for _, tc := range testCases { + newDur, err := findDurationRange(tc.in.dur, tc.in.durRanges) + + assert.Equal(t, tc.expected.dur, newDur, tc.desc) + assert.Equal(t, tc.expected.err, err, tc.desc) + } +} + +func TestSetSeatNonBid(t *testing.T) { + type args struct { + bidResponseExt *openrtb_ext.ExtBidResponse + seatNonBids nonBids + } + tests := []struct { + name string + args args + want *openrtb_ext.ExtBidResponse + }{ + { + name: "empty-seatNonBidsMap", + args: args{seatNonBids: nonBids{}, bidResponseExt: nil}, + want: nil, + }, + { + name: "nil-bidResponseExt", + args: args{seatNonBids: nonBids{seatNonBidsMap: map[string][]openrtb_ext.NonBid{"key": nil}}, bidResponseExt: nil}, + want: &openrtb_ext.ExtBidResponse{ + Prebid: &openrtb_ext.ExtResponsePrebid{ + SeatNonBid: []openrtb_ext.SeatNonBid{{ + Seat: "key", + }}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := setSeatNonBid(tt.args.bidResponseExt, tt.args.seatNonBids); !reflect.DeepEqual(got, tt.want) { + t.Errorf("setSeatNonBid() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/exchange/exchangetest/activity-tid-off.json b/exchange/exchangetest/activity-tid-off.json new file mode 100644 index 00000000000..174f09659ec --- /dev/null +++ b/exchange/exchangetest/activity-tid-off.json @@ -0,0 +1,85 @@ +{ + "accountPrivacy": { + "allowactivities": { + "transmitTid": { + "default": true, + "rules": [ + { + "allow": true, + "condition": { + "componentName": ["appnexus"], + "componentType":["bidder"] + } + } + ] + } + } + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "tid": "extTestTID", + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "source": { + "tid": "sourceTestTID" + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "tid": "extTestTID", + "bidder": { + "placementId": 1 + } + } + } + ], + "source": { + "tid": "sourceTestTID" + } + } + }, + "mockResponse": { + "errors": [ + "appnexus-error" + ] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/activity-tid-on.json b/exchange/exchangetest/activity-tid-on.json new file mode 100644 index 00000000000..98da945bff2 --- /dev/null +++ b/exchange/exchangetest/activity-tid-on.json @@ -0,0 +1,82 @@ +{ + "accountPrivacy": { + "allowactivities": { + "transmitTid": { + "default": true, + "rules": [ + { + "allow": false, + "condition": { + "componentName": ["appnexus"], + "componentType":["bidder"] + } + } + ] + } + } + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "tid": "extTestTID", + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "source": { + "tid": "sourceTestTID" + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ], + "source": {} + } + }, + "mockResponse": { + "errors": [ + "appnexus-error" + ] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/floors_enforcement.json b/exchange/exchangetest/floors_enforcement.json new file mode 100644 index 00000000000..90d25301e65 --- /dev/null +++ b/exchange/exchangetest/floors_enforcement.json @@ -0,0 +1,153 @@ +{ + "floors_enabled": true, + "account_floors_enabled": true, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + } + } + } + ], + "ext": { + "prebid": { + "floors": { + "data": { + "modelgroups": [ + { + "currency": "USD", + "modelversion": "version1", + "default": 5, + "values": { + "banner|www.website.com": 3, + "video|www.website.com": 7, + "*|*": 11 + }, + "schema": { + "fields": [ + "mediaType", + "domain" + ], + "delimiter": "|" + } + } + ] + }, + "enabled": true, + "enforcement": { + "enforcepbs": true, + "floordeals": true, + "enforcerate": 100 + } + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid1", + "impid": "my-imp-id", + "price": 12, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + } + } + ], + "seat": "appnexus", + "currency": "USD" + } + ] + } + }, + "audienceNetwork": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid2", + "impid": "my-imp-id", + "price": 7, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + } + } + ], + "seat": "audienceNetwork", + "currency": "USD" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "winning-bid1", + "impid": "my-imp-id", + "price": 12, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 12, + "prebid": { + "floors": { + "floorCurrency": "USD", + "floorRule": "*|*", + "floorRuleValue": 11, + "floorValue": 11 + } + } + } + } + ] + } + ] + } + } + } \ No newline at end of file diff --git a/exchange/exchangetest/mediatypepricegranularity-banner-video-native.json b/exchange/exchangetest/mediatypepricegranularity-banner-video-native.json new file mode 100644 index 00000000000..7d119b23044 --- /dev/null +++ b/exchange/exchangetest/mediatypepricegranularity-banner-video-native.json @@ -0,0 +1,286 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + }, + { + "id": "imp-id-3", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 2 + } + ] + }, + "mediatypepricegranularity": { + "banner": { + "precision": 3, + "ranges": [ + { + "max": 20, + "increment": 4.5 + } + ] + }, + "video": { + "precision": 4, + "ranges": [ + { + "min": 10, + "max": 30, + "increment": 1.5 + } + ] + }, + "native": { + "precision": 5, + "ranges": [ + { + "max": 30, + "increment": 1.8 + } + ] + } + }, + "includewinners": true, + "includebidderkeys": true, + "appendbiddernames": true + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 15, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "mtype": 1 + }, + "bidType": "banner" + }, + { + "ortbBid": { + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 18, + "w": 300, + "h": 500, + "crid": "creative-3", + "cat": [ + "IAB1-2" + ], + "mtype": 2 + }, + "bidType": "video" + }, + { + "ortbBid": { + "id": "apn-native-bid", + "impid": "imp-id-3", + "price": 29, + "w": 300, + "h": 500, + "crid": "creative-3", + "cat": [ + "IAB1-4" + ], + "mtype": 4 + }, + "bidType": "native" + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 15, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "mtype": 1, + "ext": { + "origbidcpm": 15, + "prebid": { + "type": "banner", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "13.500", + "hb_pb_appnexus": "13.500", + "hb_pb_cat_dur": "13.500_VideoGames_0s_appnexus", + "hb_pb_cat_dur_appnex": "13.500_VideoGames_0s_appnexus", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + }, + { + "cat": [ + "IAB1-2" + ], + "crid": "creative-3", + "mtype": 2, + "ext": { + "origbidcpm": 18, + "prebid": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "17.5000", + "hb_pb_appnexus": "17.5000", + "hb_pb_cat_dur": "17.5000_HomeDecor_0s_appnexus", + "hb_pb_cat_dur_appnex": "17.5000_HomeDecor_0s_appnexus", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + }, + "type": "video" + } + }, + "h": 500, + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 18, + "w": 300 + }, + { + "cat": [ + "IAB1-4" + ], + "crid": "creative-3", + "mtype": 4, + "ext": { + "origbidcpm": 29, + "prebid": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "28.80000", + "hb_pb_appnexus": "28.80000", + "hb_pb_cat_dur": "28.80000_Sports_0s_appnexus", + "hb_pb_cat_dur_appnex": "28.80000_Sports_0s_appnexus", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + }, + "type": "native" + } + }, + "h": 500, + "id": "apn-native-bid", + "impid": "imp-id-3", + "price": 29, + "w": 300 + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/mediatypepricegranularity-native.json b/exchange/exchangetest/mediatypepricegranularity-native.json new file mode 100644 index 00000000000..57a318e1fcf --- /dev/null +++ b/exchange/exchangetest/mediatypepricegranularity-native.json @@ -0,0 +1,222 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 2 + } + ] + }, + "mediatypepricegranularity": { + "banner": { + "precision": 3, + "ranges": [ + { + "max": 20, + "increment": 4.5 + } + ] + }, + "video": { + "precision": 4, + "ranges": [ + { + "min": 10, + "max": 30, + "increment": 1.5 + } + ] + }, + "native": { + "precision": 5, + "ranges": [ + { + "max": 30, + "increment": 1.8 + } + ] + } + }, + "includewinners": true, + "includebidderkeys": true, + "appendbiddernames": true + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 24, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "mtype": 4 + }, + "bidType": "native" + }, + { + "ortbBid": { + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 29, + "w": 300, + "h": 500, + "crid": "creative-3", + "cat": [ + "IAB1-2" + ], + "mtype": 4 + }, + "bidType": "native" + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 24, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "mtype": 4, + "ext": { + "origbidcpm": 24, + "prebid": { + "type": "native", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "23.40000", + "hb_pb_appnexus": "23.40000", + "hb_pb_cat_dur": "23.40000_VideoGames_0s_appnexus", + "hb_pb_cat_dur_appnex": "23.40000_VideoGames_0s_appnexus", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + }, + { + "cat": [ + "IAB1-2" + ], + "crid": "creative-3", + "mtype": 4, + "ext": { + "origbidcpm": 29, + "prebid": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "28.80000", + "hb_pb_appnexus": "28.80000", + "hb_pb_cat_dur": "28.80000_HomeDecor_0s_appnexus", + "hb_pb_cat_dur_appnex": "28.80000_HomeDecor_0s_appnexus", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + }, + "type": "native" + } + }, + "h": 500, + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 29, + "w": 300 + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/request-ext-prebid-filtering.json b/exchange/exchangetest/request-ext-prebid-filtering.json index 499f779af26..8e85d8edfdc 100644 --- a/exchange/exchangetest/request-ext-prebid-filtering.json +++ b/exchange/exchangetest/request-ext-prebid-filtering.json @@ -60,6 +60,17 @@ "rubicon": { "not": "permitted-for-appnexus" } + }, + "sdk": { + "renderers": [ + { + "name": "test-name", + "version": "test-version", + "data" : { + "complex": "data" + } + } + ] } } } @@ -108,6 +119,17 @@ "bidderparams": { "param1": 1, "paramA": "A" + }, + "sdk": { + "renderers": [ + { + "name": "test-name", + "version": "test-version", + "data": { + "complex": "data" + } + } + ] } } } diff --git a/exchange/gdpr.go b/exchange/gdpr.go index c8b205ad045..d503eb5da27 100644 --- a/exchange/gdpr.go +++ b/exchange/gdpr.go @@ -1,59 +1,37 @@ package exchange import ( - "encoding/json" - gpplib "github.com/prebid/go-gpp" gppConstants "github.com/prebid/go-gpp/constants" - "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/gdpr" + "github.com/prebid/prebid-server/openrtb_ext" + gppPolicy "github.com/prebid/prebid-server/privacy/gpp" ) -// ExtractGDPR will pull the gdpr flag from an openrtb request -func extractGDPR(bidRequest *openrtb2.BidRequest) (gdpr.Signal, error) { - var re regsExt - var err error - - if bidRequest.Regs != nil && len(bidRequest.Regs.GPPSID) > 0 { - for _, id := range bidRequest.Regs.GPPSID { - if id == int8(gppConstants.SectionTCFEU2) { - return gdpr.SignalYes, nil - } +// getGDPR will pull the gdpr flag from an openrtb request +func getGDPR(req *openrtb_ext.RequestWrapper) (gdpr.Signal, error) { + if req.Regs != nil && len(req.Regs.GPPSID) > 0 { + if gppPolicy.IsSIDInList(req.Regs.GPPSID, gppConstants.SectionTCFEU2) { + return gdpr.SignalYes, nil } return gdpr.SignalNo, nil } - if bidRequest.Regs != nil && bidRequest.Regs.Ext != nil { - err = json.Unmarshal(bidRequest.Regs.Ext, &re) - } - if re.GDPR == nil || err != nil { + re, err := req.GetRegExt() + if re == nil || re.GetGDPR() == nil || err != nil { return gdpr.SignalAmbiguous, err } - return gdpr.Signal(*re.GDPR), nil + return gdpr.Signal(*re.GetGDPR()), nil } -// ExtractConsent will pull the consent string from an openrtb request -func extractConsent(bidRequest *openrtb2.BidRequest, gpp gpplib.GppContainer) (consent string, err error) { - for i, id := range gpp.SectionTypes { - if id == gppConstants.SectionTCFEU2 { - consent = gpp.Sections[i].GetValue() - return - } - } - var ue userExt - if bidRequest.User != nil && bidRequest.User.Ext != nil { - err = json.Unmarshal(bidRequest.User.Ext, &ue) +// getConsent will pull the consent string from an openrtb request +func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (consent string, err error) { + if i := gppPolicy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 { + consent = gpp.Sections[i].GetValue() + return } - if err != nil { + ue, err := req.GetUserExt() + if ue == nil || ue.GetConsent() == nil || err != nil { return } - consent = ue.Consent - return -} - -type userExt struct { - Consent string `json:"consent,omitempty"` -} - -type regsExt struct { - GDPR *int `json:"gdpr,omitempty"` + return *ue.GetConsent(), nil } diff --git a/exchange/gdpr_test.go b/exchange/gdpr_test.go index 425538749a5..44573b59167 100644 --- a/exchange/gdpr_test.go +++ b/exchange/gdpr_test.go @@ -8,10 +8,11 @@ import ( gppConstants "github.com/prebid/go-gpp/constants" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/gdpr" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) -func TestExtractGDPR(t *testing.T) { +func TestGetGDPR(t *testing.T) { tests := []struct { description string giveRegs *openrtb2.Regs @@ -78,11 +79,12 @@ func TestExtractGDPR(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - bidReq := openrtb2.BidRequest{ - Regs: tt.giveRegs, + req := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: tt.giveRegs, + }, } - - result, err := extractGDPR(&bidReq) + result, err := getGDPR(&req) assert.Equal(t, tt.wantGDPR, result) if tt.wantError { @@ -94,7 +96,7 @@ func TestExtractGDPR(t *testing.T) { } } -func TestExtractConsent(t *testing.T) { +func TestGetConsent(t *testing.T) { tests := []struct { description string giveUser *openrtb2.User @@ -150,11 +152,13 @@ func TestExtractConsent(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - bidReq := openrtb2.BidRequest{ - User: tt.giveUser, + req := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + User: tt.giveUser, + }, } - result, err := extractConsent(&bidReq, tt.giveGPP) + result, err := getConsent(&req, tt.giveGPP) assert.Equal(t, tt.wantConsent, result, tt.description) if tt.wantError { diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go new file mode 100644 index 00000000000..9a6f1f6bf61 --- /dev/null +++ b/exchange/non_bid_reason.go @@ -0,0 +1,24 @@ +package exchange + +// SeatNonBid list the reasons why bid was not resulted in positive bid +// reason could be either No bid, Error, Request rejection or Response rejection +// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md +type NonBidReason int + +const ( + NoBidUnknownError NonBidReason = 0 // No Bid - General + ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid +) + +// Ptr returns pointer to own value. +func (n NonBidReason) Ptr() *NonBidReason { + return &n +} + +// Val safely dereferences pointer, returning default value (NoBidUnknownError) for nil. +func (n *NonBidReason) Val() NonBidReason { + if n == nil { + return NoBidUnknownError + } + return *n +} diff --git a/exchange/price_granularity.go b/exchange/price_granularity.go index 1af56ea2aca..af9e46b20fe 100644 --- a/exchange/price_granularity.go +++ b/exchange/price_granularity.go @@ -1,20 +1,34 @@ package exchange import ( + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" "math" "strconv" - - "github.com/prebid/prebid-server/openrtb_ext" ) // GetPriceBucket is the externally facing function for computing CPM buckets -func GetPriceBucket(cpm float64, config openrtb_ext.PriceGranularity) string { +func GetPriceBucket(bid openrtb2.Bid, targetingData targetData) string { cpmStr := "" bucketMax := 0.0 bucketMin := 0.0 increment := 0.0 + + config := targetingData.priceGranularity //assign default price granularity + + if bidType, err := getMediaTypeForBid(bid); err == nil { + if bidType == openrtb_ext.BidTypeBanner && targetingData.mediaTypePriceGranularity.Banner != nil { + config = *targetingData.mediaTypePriceGranularity.Banner + } else if bidType == openrtb_ext.BidTypeVideo && targetingData.mediaTypePriceGranularity.Video != nil { + config = *targetingData.mediaTypePriceGranularity.Video + } else if bidType == openrtb_ext.BidTypeNative && targetingData.mediaTypePriceGranularity.Native != nil { + config = *targetingData.mediaTypePriceGranularity.Native + } + } + precision := *config.Precision + cpm := bid.Price for i := 0; i < len(config.Ranges); i++ { if config.Ranges[i].Max > bucketMax { bucketMax = config.Ranges[i].Max diff --git a/exchange/price_granularity_test.go b/exchange/price_granularity_test.go index 50e83778550..4f9337aadc3 100644 --- a/exchange/price_granularity_test.go +++ b/exchange/price_granularity_test.go @@ -1,9 +1,11 @@ package exchange import ( + "encoding/json" "math" "testing" + "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" @@ -51,112 +53,149 @@ func TestGetPriceBucketString(t *testing.T) { // Define test cases type aTest struct { granularityId string - granularity openrtb_ext.PriceGranularity + targetData targetData expectedPriceBucket string } testGroups := []struct { groupDesc string - cpm float64 + bid openrtb2.Bid testCases []aTest }{ { groupDesc: "cpm below the max in every price bucket", - cpm: 1.87, + bid: openrtb2.Bid{Price: 1.87}, testCases: []aTest{ - {"low", low, "1.50"}, - {"medium", medium, "1.80"}, - {"high", high, "1.87"}, - {"auto", auto, "1.85"}, - {"dense", dense, "1.87"}, - {"custom1", custom1, "1.86"}, - {"custom2", custom2, "1.50"}, + {"low", targetData{priceGranularity: low}, "1.50"}, + {"medium", targetData{priceGranularity: medium}, "1.80"}, + {"high", targetData{priceGranularity: high}, "1.87"}, + {"auto", targetData{priceGranularity: auto}, "1.85"}, + {"dense", targetData{priceGranularity: dense}, "1.87"}, + {"custom1", targetData{priceGranularity: custom1}, "1.86"}, + {"custom2", targetData{priceGranularity: custom2}, "1.50"}, }, }, { groupDesc: "cpm above the max in low price bucket", - cpm: 5.72, + bid: openrtb2.Bid{Price: 5.72}, testCases: []aTest{ - {"low", low, "5.00"}, - {"medium", medium, "5.70"}, - {"high", high, "5.72"}, - {"auto", auto, "5.70"}, - {"dense", dense, "5.70"}, - {"custom1", custom1, "5.70"}, - {"custom2", custom2, "5.10"}, + {"low", targetData{priceGranularity: low}, "5.00"}, + {"medium", targetData{priceGranularity: medium}, "5.70"}, + {"high", targetData{priceGranularity: high}, "5.72"}, + {"auto", targetData{priceGranularity: auto}, "5.70"}, + {"dense", targetData{priceGranularity: dense}, "5.70"}, + {"custom1", targetData{priceGranularity: custom1}, "5.70"}, + {"custom2", targetData{priceGranularity: custom2}, "5.10"}, + }, + }, + { + groupDesc: "media type price granularity for bid type video", + bid: openrtb2.Bid{Price: 5.0, MType: openrtb2.MarkupVideo}, + testCases: []aTest{ + {"medium", targetData{priceGranularity: medium}, "5.00"}, + {"video-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Video: &custom2}}, "3.90"}, + {"banner-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Banner: &custom2}}, "5.00"}, + }, + }, + { + groupDesc: "media type price granularity for bid type banner", + bid: openrtb2.Bid{Price: 5.0, MType: openrtb2.MarkupBanner}, + testCases: []aTest{ + {"medium", targetData{priceGranularity: medium}, "5.00"}, + {"video-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Video: &custom2}}, "5.00"}, + {"banner-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Banner: &custom2}}, "3.90"}, + }, + }, + { + groupDesc: "media type price granularity for bid type native", + bid: openrtb2.Bid{Price: 5.0, MType: openrtb2.MarkupNative}, + testCases: []aTest{ + {"medium", targetData{priceGranularity: medium}, "5.00"}, + {"video-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Video: &custom2}}, "5.00"}, + {"native-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Native: &custom2}}, "3.90"}, + }, + }, + { + groupDesc: "media type price granularity set but bid type incorrect", + bid: openrtb2.Bid{Price: 5.0, Ext: json.RawMessage(`{`)}, + testCases: []aTest{ + {"medium", targetData{priceGranularity: medium}, "5.00"}, + {"video-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Video: &custom2}}, "5.00"}, + {"banner-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Banner: &custom2}}, "5.00"}, + {"native-custom2", targetData{priceGranularity: medium, mediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{Native: &custom2}}, "5.00"}, }, }, { groupDesc: "cpm equal the max for custom granularity", - cpm: 10, + bid: openrtb2.Bid{Price: 10}, testCases: []aTest{ - {"custom1", custom1, "10.00"}, - {"custom2", custom2, "9.90"}, + {"custom1", targetData{priceGranularity: custom1}, "10.00"}, + {"custom2", targetData{priceGranularity: custom2}, "9.90"}, }, }, { groupDesc: "Precision value corner cases", - cpm: 1.876, + bid: openrtb2.Bid{Price: 1.876}, testCases: []aTest{ { "Negative precision defaults to number of digits already in CPM float", - openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(-1), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}, + targetData{priceGranularity: openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(-1), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}}, "1.85", }, { "Precision value equals zero, we expect to round up to the nearest integer", - openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(0), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}, + targetData{priceGranularity: openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(0), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}}, "2", }, { "Largest precision value PBS supports 15", - openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(15), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}, + targetData{priceGranularity: openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(15), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}}, "1.850000000000000", }, }, }, { groupDesc: "Increment value corner cases", - cpm: 1.876, + bid: openrtb2.Bid{Price: 1.876}, testCases: []aTest{ { "Negative increment, return empty string", - openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}}, + targetData{priceGranularity: openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}}}, "", }, { "Zero increment, return empty string", - openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5}}}, + targetData{priceGranularity: openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5}}}}, "", }, { "Increment value is greater than CPM itself, return zero float value", - openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}}, + targetData{priceGranularity: openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}}}, "0.00", }, }, }, { groupDesc: "Negative Cpm, return empty string since it does not belong into any range", - cpm: -1.876, - testCases: []aTest{{"low", low, ""}}, + bid: openrtb2.Bid{Price: -1.876}, + testCases: []aTest{{"low", targetData{priceGranularity: low}, ""}}, }, { groupDesc: "Zero value Cpm, return the same, only in string format", - cpm: 0, - testCases: []aTest{{"low", low, "0.00"}}, + bid: openrtb2.Bid{Price: 0}, + testCases: []aTest{{"low", targetData{priceGranularity: low}, "0.00"}}, }, { groupDesc: "Large Cpm, return bucket Max", - cpm: math.MaxFloat64, - testCases: []aTest{{"low", low, "5.00"}}, + bid: openrtb2.Bid{Price: math.MaxFloat64}, + testCases: []aTest{{"low", targetData{priceGranularity: low}, "5.00"}}, }, } for _, testGroup := range testGroups { for i, test := range testGroup.testCases { var priceBucket string - assert.NotPanics(t, func() { priceBucket = GetPriceBucket(testGroup.cpm, test.granularity) }, "Group: %s Granularity: %d", testGroup.groupDesc, i) - assert.Equal(t, test.expectedPriceBucket, priceBucket, "Group: %s Granularity: %s :: Expected %s, got %s from %f", testGroup.groupDesc, test.granularityId, test.expectedPriceBucket, priceBucket, testGroup.cpm) + assert.NotPanics(t, func() { priceBucket = GetPriceBucket(testGroup.bid, test.targetData) }, "Group: %s Granularity: %d", testGroup.groupDesc, i) + assert.Equal(t, test.expectedPriceBucket, priceBucket, "Group: %s Granularity: %s :: Expected %s, got %s from %f", testGroup.groupDesc, test.granularityId, test.expectedPriceBucket, priceBucket, testGroup.bid.Price) } } } diff --git a/exchange/seat_non_bids.go b/exchange/seat_non_bids.go new file mode 100644 index 00000000000..463a4595c85 --- /dev/null +++ b/exchange/seat_non_bids.go @@ -0,0 +1,55 @@ +package exchange + +import ( + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type nonBids struct { + seatNonBidsMap map[string][]openrtb_ext.NonBid +} + +// addBid is not thread safe as we are initializing and writing to map +func (snb *nonBids) addBid(bid *entities.PbsOrtbBid, nonBidReason int, seat string) { + if bid == nil || bid.Bid == nil { + return + } + if snb.seatNonBidsMap == nil { + snb.seatNonBidsMap = make(map[string][]openrtb_ext.NonBid) + } + nonBid := openrtb_ext.NonBid{ + ImpId: bid.Bid.ImpID, + StatusCode: nonBidReason, + Ext: openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{ + Price: bid.Bid.Price, + ADomain: bid.Bid.ADomain, + CatTax: bid.Bid.CatTax, + Cat: bid.Bid.Cat, + DealID: bid.Bid.DealID, + W: bid.Bid.W, + H: bid.Bid.H, + Dur: bid.Bid.Dur, + MType: bid.Bid.MType, + OriginalBidCPM: bid.OriginalBidCPM, + OriginalBidCur: bid.OriginalBidCur, + }}, + }, + } + + snb.seatNonBidsMap[seat] = append(snb.seatNonBidsMap[seat], nonBid) +} + +func (snb *nonBids) get() []openrtb_ext.SeatNonBid { + if snb == nil { + return nil + } + var seatNonBid []openrtb_ext.SeatNonBid + for seat, nonBids := range snb.seatNonBidsMap { + seatNonBid = append(seatNonBid, openrtb_ext.SeatNonBid{ + Seat: seat, + NonBid: nonBids, + }) + } + return seatNonBid +} diff --git a/exchange/seat_non_bids_test.go b/exchange/seat_non_bids_test.go new file mode 100644 index 00000000000..d9f7aa88ca0 --- /dev/null +++ b/exchange/seat_non_bids_test.go @@ -0,0 +1,110 @@ +package exchange + +import ( + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestSeatNonBidsAdd(t *testing.T) { + type fields struct { + seatNonBidsMap map[string][]openrtb_ext.NonBid + } + type args struct { + bid *entities.PbsOrtbBid + nonBidReason int + seat string + } + tests := []struct { + name string + fields fields + args args + want map[string][]openrtb_ext.NonBid + }{ + { + name: "nil-seatNonBidsMap", + fields: fields{seatNonBidsMap: nil}, + args: args{}, + want: nil, + }, + { + name: "nil-seatNonBidsMap-with-bid-object", + fields: fields{seatNonBidsMap: nil}, + args: args{bid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{}}, seat: "bidder1"}, + want: sampleSeatNonBidMap("bidder1", 1), + }, + { + name: "multiple-nonbids-for-same-seat", + fields: fields{seatNonBidsMap: sampleSeatNonBidMap("bidder2", 1)}, + args: args{bid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{}}, seat: "bidder2"}, + want: sampleSeatNonBidMap("bidder2", 2), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + snb := &nonBids{ + seatNonBidsMap: tt.fields.seatNonBidsMap, + } + snb.addBid(tt.args.bid, tt.args.nonBidReason, tt.args.seat) + assert.Equalf(t, tt.want, snb.seatNonBidsMap, "expected seatNonBidsMap not nil") + }) + } +} + +func TestSeatNonBidsGet(t *testing.T) { + type fields struct { + snb *nonBids + } + tests := []struct { + name string + fields fields + want []openrtb_ext.SeatNonBid + }{ + { + name: "get-seat-nonbids", + fields: fields{&nonBids{sampleSeatNonBidMap("bidder1", 2)}}, + want: sampleSeatBids("bidder1", 2), + }, + { + name: "nil-seat-nonbids", + fields: fields{nil}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.fields.snb.get(); !assert.Equal(t, tt.want, got) { + t.Errorf("seatNonBids.get() = %v, want %v", got, tt.want) + } + }) + } +} + +var sampleSeatNonBidMap = func(seat string, nonBidCount int) map[string][]openrtb_ext.NonBid { + nonBids := make([]openrtb_ext.NonBid, 0) + for i := 0; i < nonBidCount; i++ { + nonBids = append(nonBids, openrtb_ext.NonBid{ + Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + }) + } + return map[string][]openrtb_ext.NonBid{ + seat: nonBids, + } +} + +var sampleSeatBids = func(seat string, nonBidCount int) []openrtb_ext.SeatNonBid { + seatNonBids := make([]openrtb_ext.SeatNonBid, 0) + seatNonBid := openrtb_ext.SeatNonBid{ + Seat: seat, + NonBid: make([]openrtb_ext.NonBid, 0), + } + for i := 0; i < nonBidCount; i++ { + seatNonBid.NonBid = append(seatNonBid.NonBid, openrtb_ext.NonBid{ + Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + }) + } + seatNonBids = append(seatNonBids, seatNonBid) + return seatNonBids +} diff --git a/exchange/targeting.go b/exchange/targeting.go index 987c6aa6e7c..dbbf10041c9 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -18,13 +18,14 @@ const MaxKeyLength = 20 // All functions on this struct are all nil-safe. // If the value is nil, then no targeting data will be tracked. type targetData struct { - priceGranularity openrtb_ext.PriceGranularity - includeWinners bool - includeBidderKeys bool - includeCacheBids bool - includeCacheVast bool - includeFormat bool - preferDeals bool + priceGranularity openrtb_ext.PriceGranularity + mediaTypePriceGranularity openrtb_ext.MediaTypePriceGranularity + includeWinners bool + includeBidderKeys bool + includeCacheBids bool + includeCacheVast bool + includeFormat bool + preferDeals bool // cacheHost and cachePath exist to supply cache host and path as targeting parameters cacheHost string cachePath string diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 5ea508b9cc1..a5f49689349 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -91,9 +91,6 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder ex := &exchange{ adapterMap: buildAdapterMap(mockBids, server.URL, server.Client()), @@ -101,16 +98,14 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op cache: &wellBehavedCache{}, cacheTime: time.Duration(0), gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), gdprDefaultValue: gdpr.SignalYes, categoriesFetcher: categoriesFetcher, bidIDGenerator: &mockBidIDGenerator{false, false}, } ex.requestSplitter = requestSplitter{ - me: ex.me, - gdprPermsBuilder: ex.gdprPermsBuilder, - tcf2ConfigBuilder: ex.tcf2ConfigBuilder, + me: ex.me, + gdprPermsBuilder: ex.gdprPermsBuilder, } imps := buildImps(t, mockBids) @@ -125,11 +120,12 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op req.Site = &openrtb2.Site{} } - auctionRequest := AuctionRequest{ + auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, Account: config.Account{}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } debugLog := DebugLog{} @@ -918,7 +914,7 @@ func TestSetTargeting(t *testing.T) { for _, test := range TargetingTests { auc := &test.Auction // Set rounded prices from the auction data - auc.setRoundedPrices(test.TargetData.priceGranularity) + auc.setRoundedPrices(test.TargetData) winningBids := make(map[string]*entities.PbsOrtbBid) // Set winning bids from the auction data for imp, bidsByBidder := range auc.winningBidsByBidder { diff --git a/exchange/tmax_adjustments.go b/exchange/tmax_adjustments.go new file mode 100644 index 00000000000..29e732995af --- /dev/null +++ b/exchange/tmax_adjustments.go @@ -0,0 +1,63 @@ +package exchange + +import ( + "context" + "time" + + "github.com/prebid/prebid-server/config" +) + +type TmaxAdjustmentsPreprocessed struct { + BidderNetworkLatencyBuffer uint + PBSResponsePreparationDuration uint + BidderResponseDurationMin uint + + IsEnforced bool +} + +func ProcessTMaxAdjustments(adjustmentsConfig config.TmaxAdjustments) *TmaxAdjustmentsPreprocessed { + if !adjustmentsConfig.Enabled { + return nil + } + + isEnforced := adjustmentsConfig.BidderResponseDurationMin != 0 && + (adjustmentsConfig.BidderNetworkLatencyBuffer != 0 || adjustmentsConfig.PBSResponsePreparationDuration != 0) + + tmax := &TmaxAdjustmentsPreprocessed{ + BidderNetworkLatencyBuffer: adjustmentsConfig.BidderNetworkLatencyBuffer, + PBSResponsePreparationDuration: adjustmentsConfig.PBSResponsePreparationDuration, + BidderResponseDurationMin: adjustmentsConfig.BidderResponseDurationMin, + IsEnforced: isEnforced, + } + + return tmax +} + +type bidderTmaxContext interface { + Deadline() (deadline time.Time, ok bool) + RemainingDurationMS(deadline time.Time) int64 + Until(t time.Time) time.Duration +} +type bidderTmaxCtx struct{ ctx context.Context } + +func (b *bidderTmaxCtx) RemainingDurationMS(deadline time.Time) int64 { + return time.Until(deadline).Milliseconds() +} +func (b *bidderTmaxCtx) Deadline() (deadline time.Time, ok bool) { + deadline, ok = b.ctx.Deadline() + return +} + +// Until returns the remaining duration until the specified time +func (b *bidderTmaxCtx) Until(t time.Time) time.Duration { + return time.Until(t) +} + +func getBidderTmax(ctx bidderTmaxContext, requestTmaxMS int64, tmaxAdjustments TmaxAdjustmentsPreprocessed) int64 { + if tmaxAdjustments.IsEnforced { + if deadline, ok := ctx.Deadline(); ok { + return ctx.RemainingDurationMS(deadline) - int64(tmaxAdjustments.BidderNetworkLatencyBuffer) - int64(tmaxAdjustments.PBSResponsePreparationDuration) + } + } + return requestTmaxMS +} diff --git a/exchange/tmax_adjustments_test.go b/exchange/tmax_adjustments_test.go new file mode 100644 index 00000000000..7e6a02ab81e --- /dev/null +++ b/exchange/tmax_adjustments_test.go @@ -0,0 +1,108 @@ +package exchange + +import ( + "testing" + "time" + + "github.com/prebid/prebid-server/config" + "github.com/stretchr/testify/assert" +) + +func TestGetBidderTmax(t *testing.T) { + var ( + requestTmaxMS int64 = 700 + bidderNetworkLatencyBuffer uint = 50 + responsePreparationDuration uint = 60 + ) + requestTmaxNS := requestTmaxMS * int64(time.Millisecond) + startTime := time.Date(2023, 5, 30, 1, 0, 0, 0, time.UTC) + deadline := time.Date(2023, 5, 30, 1, 0, 0, int(requestTmaxNS), time.UTC) + ctx := &mockBidderTmaxCtx{startTime: startTime, deadline: deadline, ok: true} + tests := []struct { + description string + ctx bidderTmaxContext + requestTmax int64 + expectedTmax int64 + tmaxAdjustments TmaxAdjustmentsPreprocessed + }{ + { + description: "returns-requestTmax-when-IsEnforced-is-false", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: false}, + expectedTmax: requestTmaxMS, + }, + { + description: "returns-requestTmax-when-BidderResponseDurationMin-is-not-set", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 0}, + expectedTmax: requestTmaxMS, + }, + { + description: "returns-requestTmax-when-BidderNetworkLatencyBuffer-and-PBSResponsePreparationDuration-is-not-set", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 0, PBSResponsePreparationDuration: 0}, + expectedTmax: requestTmaxMS, + }, + { + description: "returns-requestTmax-when-context-deadline-is-not-set", + ctx: &mockBidderTmaxCtx{ok: false}, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 50, PBSResponsePreparationDuration: 60}, + expectedTmax: requestTmaxMS, + }, + { + description: "returns-remaing-duration-by-subtracting-BidderNetworkLatencyBuffer-and-PBSResponsePreparationDuration", + ctx: ctx, + requestTmax: requestTmaxMS, + tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: bidderNetworkLatencyBuffer, PBSResponsePreparationDuration: responsePreparationDuration}, + expectedTmax: ctx.RemainingDurationMS(deadline) - int64(bidderNetworkLatencyBuffer) - int64(responsePreparationDuration), + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + assert.Equal(t, test.expectedTmax, getBidderTmax(test.ctx, test.requestTmax, test.tmaxAdjustments)) + }) + } +} + +func TestProcessTMaxAdjustments(t *testing.T) { + tests := []struct { + description string + expected *TmaxAdjustmentsPreprocessed + tmaxAdjustments config.TmaxAdjustments + }{ + { + description: "returns-nil-when-tmax-is-not-enabled", + tmaxAdjustments: config.TmaxAdjustments{Enabled: false}, + expected: nil, + }, + { + description: "BidderResponseDurationMin-is-not-set", + tmaxAdjustments: config.TmaxAdjustments{Enabled: true, BidderResponseDurationMin: 0, BidderNetworkLatencyBuffer: 10, PBSResponsePreparationDuration: 20}, + expected: &TmaxAdjustmentsPreprocessed{IsEnforced: false, BidderResponseDurationMin: 0, BidderNetworkLatencyBuffer: 10, PBSResponsePreparationDuration: 20}, + }, + { + description: "BidderNetworkLatencyBuffer-and-PBSResponsePreparationDuration-are-not-set", + tmaxAdjustments: config.TmaxAdjustments{Enabled: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 0, PBSResponsePreparationDuration: 0}, + expected: &TmaxAdjustmentsPreprocessed{IsEnforced: false, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 0, PBSResponsePreparationDuration: 0}, + }, + { + description: "BidderNetworkLatencyBuffer-is-not-set", + tmaxAdjustments: config.TmaxAdjustments{Enabled: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 0, PBSResponsePreparationDuration: 10}, + expected: &TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 0, PBSResponsePreparationDuration: 10}, + }, + { + description: "PBSResponsePreparationDuration-is-not-set", + tmaxAdjustments: config.TmaxAdjustments{Enabled: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 10, PBSResponsePreparationDuration: 0}, + expected: &TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 10, PBSResponsePreparationDuration: 0}, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + assert.Equal(t, test.expected, ProcessTMaxAdjustments(test.tmaxAdjustments)) + }) + } +} diff --git a/exchange/utils.go b/exchange/utils.go index d6b30a8c1d0..267d928ca72 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "math/rand" @@ -13,8 +14,8 @@ import ( gppConstants "github.com/prebid/go-gpp/constants" "github.com/prebid/openrtb/v19/openrtb2" - "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/firstpartydata" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" @@ -41,7 +42,6 @@ type requestSplitter struct { me metrics.MetricsEngine privacyConfig config.Privacy gdprPermsBuilder gdpr.PermissionsBuilder - tcf2ConfigBuilder gdpr.TCF2ConfigBuilder hostSChainNode *openrtb2.SupplyChainNode bidderInfo config.BidderInfos } @@ -93,12 +93,12 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, } } - gdprSignal, err := extractGDPR(req.BidRequest) + gdprSignal, err := getGDPR(req) if err != nil { errs = append(errs, err) } - consent, err := extractConsent(req.BidRequest, gpp) + consent, err := getConsent(req, gpp) if err != nil { errs = append(errs, err) } @@ -112,23 +112,19 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, lmtEnforcer := extractLMT(req.BidRequest, rs.privacyConfig) // request level privacy policies - privacyEnforcement := privacy.Enforcement{ - COPPA: req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1, - LMT: lmtEnforcer.ShouldEnforce(unknownBidder), - } + coppa := req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1 + lmt := lmtEnforcer.ShouldEnforce(unknownBidder) privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce() privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder) - privacyLabels.COPPAEnforced = privacyEnforcement.COPPA - privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder) - - tcf2Cfg := rs.tcf2ConfigBuilder(rs.privacyConfig.GDPR.TCF2, auctionReq.Account.GDPR) + privacyLabels.COPPAEnforced = coppa + privacyLabels.LMTEnforced = lmt var gdprEnforced bool var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{} if gdprApplies { - gdprEnforced = tcf2Cfg.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType]) + gdprEnforced = auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType]) } if gdprEnforced { @@ -145,42 +141,82 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, GDPRSignal: gdprSignal, PublisherID: auctionReq.LegacyLabels.PubID, } - gdprPerms = rs.gdprPermsBuilder(tcf2Cfg, gdprRequestInfo) + gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) } // bidder level privacy policies for _, bidderRequest := range allBidderRequests { - bidRequestAllowed := true + privacyEnforcement := privacy.Enforcement{ + COPPA: coppa, + LMT: lmt, + } + + // fetchBids activity + scopedName := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderRequest.BidderName.String()} + fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids, scopedName) + if !fetchBidsActivityAllowed { + // skip the call to a bidder if fetchBids activity is not allowed + // do not add this bidder to allowedBidderRequests + continue + } - // CCPA - privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) + var auctionPermissions gdpr.AuctionPermissions + var gdprErr error - // GDPR if gdprEnforced { - auctionPermissions, err := gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName) - bidRequestAllowed = auctionPermissions.AllowBidRequest - - if err == nil { - privacyEnforcement.GDPRGeo = !auctionPermissions.PassGeo - privacyEnforcement.GDPRID = !auctionPermissions.PassID - } else { - privacyEnforcement.GDPRGeo = true - privacyEnforcement.GDPRID = true + auctionPermissions, gdprErr = gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName) + if !auctionPermissions.AllowBidRequest { + // auction request is not permitted by GDPR + // do not add this bidder to allowedBidderRequests + rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName) + continue } + } - if !bidRequestAllowed { - rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName) + passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName) + if !passIDActivityAllowed { + privacyEnforcement.UFPD = true + } else { + // run existing policies (GDPR, CCPA, COPPA, LMT) + // potentially block passing IDs based on GDPR + if gdprEnforced { + if gdprErr == nil { + privacyEnforcement.GDPRID = !auctionPermissions.PassID + } else { + privacyEnforcement.GDPRID = true + } } + // potentially block passing IDs based on CCPA + privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) + } + + passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scopedName) + if !passGeoActivityAllowed { + privacyEnforcement.PreciseGeo = true + } else { + // run existing policies (GDPR, CCPA, COPPA, LMT) + // potentially block passing geo based on GDPR + if gdprEnforced { + if gdprErr == nil { + privacyEnforcement.GDPRGeo = !auctionPermissions.PassGeo + } else { + privacyEnforcement.GDPRGeo = true + } + } + // potentially block passing geo based on CCPA + privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) + } if auctionReq.FirstPartyData != nil && auctionReq.FirstPartyData[bidderRequest.BidderName] != nil { applyFPD(auctionReq.FirstPartyData[bidderRequest.BidderName], bidderRequest.BidRequest) } - if bidRequestAllowed { - privacyEnforcement.Apply(bidderRequest.BidRequest) - allowedBidderRequests = append(allowedBidderRequests, bidderRequest) - } + privacyEnforcement.TID = !auctionReq.Activities.Allow(privacy.ActivityTransmitTids, scopedName) + + privacyEnforcement.Apply(bidderRequest.BidRequest, auctionReq.Account.Privacy) + allowedBidderRequests = append(allowedBidderRequests, bidderRequest) + // GPP downgrade: always downgrade unless we can confirm GPP is supported if shouldSetLegacyPrivacy(rs.bidderInfo, string(bidderRequest.BidderCoreName)) { setLegacyGDPRFromGPP(bidderRequest.BidRequest, gpp) @@ -235,6 +271,32 @@ func extractLMT(orig *openrtb2.BidRequest, privacyConfig config.Privacy) privacy } } +func ExtractReqExtBidderParamsMap(bidRequest *openrtb2.BidRequest) (map[string]json.RawMessage, error) { + if bidRequest == nil { + return nil, errors.New("error bidRequest should not be nil") + } + + reqExt := &openrtb_ext.ExtRequest{} + if len(bidRequest.Ext) > 0 { + err := json.Unmarshal(bidRequest.Ext, &reqExt) + if err != nil { + return nil, fmt.Errorf("error decoding Request.ext : %s", err.Error()) + } + } + + if reqExt.Prebid.BidderParams == nil { + return nil, nil + } + + var bidderParams map[string]json.RawMessage + err := json.Unmarshal(reqExt.Prebid.BidderParams, &bidderParams) + if err != nil { + return nil, err + } + + return bidderParams, nil +} + func getAuctionBidderRequests(auctionRequest AuctionRequest, requestExt *openrtb_ext.ExtRequest, bidderToSyncerKey map[string]string, @@ -249,7 +311,7 @@ func getAuctionBidderRequests(auctionRequest AuctionRequest, return nil, []error{err} } - bidderParamsInReqExt, err := adapters.ExtractReqExtBidderParamsMap(req.BidRequest) + bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) if err != nil { return nil, []error{err} } @@ -330,12 +392,13 @@ func buildRequestExtForBidder(bidder string, requestExt json.RawMessage, request } if requestExtParsed != nil { - prebid.CurrencyConversions = requestExtParsed.Prebid.CurrencyConversions - prebid.Integration = requestExtParsed.Prebid.Integration prebid.Channel = requestExtParsed.Prebid.Channel + prebid.CurrencyConversions = requestExtParsed.Prebid.CurrencyConversions prebid.Debug = requestExtParsed.Prebid.Debug - prebid.Server = requestExtParsed.Prebid.Server + prebid.Integration = requestExtParsed.Prebid.Integration prebid.MultiBid = buildRequestExtMultiBid(bidder, requestExtParsed.Prebid.MultiBid, alternateBidderCodes) + prebid.Sdk = requestExtParsed.Prebid.Sdk + prebid.Server = requestExtParsed.Prebid.Server } // Marshal New Prebid Object @@ -781,13 +844,14 @@ func getExtCacheInstructions(requestExtPrebid *openrtb_ext.ExtRequestPrebid) ext func getExtTargetData(requestExtPrebid *openrtb_ext.ExtRequestPrebid, cacheInstructions extCacheInstructions) *targetData { if requestExtPrebid != nil && requestExtPrebid.Targeting != nil { return &targetData{ - includeWinners: *requestExtPrebid.Targeting.IncludeWinners, - includeBidderKeys: *requestExtPrebid.Targeting.IncludeBidderKeys, - includeCacheBids: cacheInstructions.cacheBids, - includeCacheVast: cacheInstructions.cacheVAST, - includeFormat: requestExtPrebid.Targeting.IncludeFormat, - priceGranularity: *requestExtPrebid.Targeting.PriceGranularity, - preferDeals: requestExtPrebid.Targeting.PreferDeals, + includeWinners: *requestExtPrebid.Targeting.IncludeWinners, + includeBidderKeys: *requestExtPrebid.Targeting.IncludeBidderKeys, + includeCacheBids: cacheInstructions.cacheBids, + includeCacheVast: cacheInstructions.cacheVAST, + includeFormat: requestExtPrebid.Targeting.IncludeFormat, + priceGranularity: *requestExtPrebid.Targeting.PriceGranularity, + mediaTypePriceGranularity: requestExtPrebid.Targeting.MediaTypePriceGranularity, + preferDeals: requestExtPrebid.Targeting.PreferDeals, } } @@ -957,3 +1021,53 @@ func WrapJSONInData(data []byte) []byte { res = append(res, []byte(`}`)...) return res } + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + mType := bid.MType + var bidType openrtb_ext.BidType + if mType > 0 { + switch mType { + case openrtb2.MarkupBanner: + bidType = openrtb_ext.BidTypeBanner + case openrtb2.MarkupVideo: + bidType = openrtb_ext.BidTypeVideo + case openrtb2.MarkupAudio: + bidType = openrtb_ext.BidTypeAudio + case openrtb2.MarkupNative: + bidType = openrtb_ext.BidTypeNative + default: + return bidType, fmt.Errorf("Failed to parse bid mType for impression \"%s\"", bid.ImpID) + } + } else { + var err error + bidType, err = getPrebidMediaTypeForBid(bid) + if err != nil { + return bidType, err + } + } + return bidType, nil +} + +func getPrebidMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + var err error + var bidType openrtb_ext.BidType + + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err = json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + if bidType, err = openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)); err == nil { + return bidType, nil + } + } + } + + errMsg := fmt.Sprintf("Failed to parse bid mediatype for impression \"%s\"", bid.ImpID) + if err != nil { + errMsg = fmt.Sprintf("%s, %s", errMsg, err.Error()) + } + + return bidType, &errortypes.BadServerResponse{ + Message: errMsg, + } +} diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 59b9c8587c7..1ab86fcd9e7 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -17,6 +17,7 @@ import ( "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -39,8 +40,8 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return true, nil } -func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { - permissions = gdpr.AuctionPermissions{ +func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (gdpr.AuctionPermissions, error) { + permissions := gdpr.AuctionPermissions{ PassGeo: p.passGeo, PassID: p.passID, } @@ -67,22 +68,12 @@ func (fpb fakePermissionsBuilder) Builder(gdpr.TCF2ConfigReader, gdpr.RequestInf return fpb.permissions } -type fakeTCF2ConfigBuilder struct { - cfg gdpr.TCF2ConfigReader -} - -func (fcr fakeTCF2ConfigBuilder) Builder(hostConfig config.TCF2, accountConfig config.AccountGDPR) gdpr.TCF2ConfigReader { - return fcr.cfg -} - func assertReq(t *testing.T, bidderRequests []BidderRequest, applyCOPPA bool, consentedVendors map[string]bool) { // assert individual bidder requests assert.NotEqual(t, bidderRequests, 0, "cleanOpenRTBRequest should split request into individual bidder requests") // assert for PI data - // Both appnexus and brightroll should be allowed since brightroll - // is used as an alias for appnexus in the test request for _, req := range bidderRequests { if !applyCOPPA && consentedVendors[req.BidderName.String()] { assert.NotEqual(t, req.BidRequest.User.BuyerUID, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA") @@ -444,6 +435,8 @@ func TestCreateSanitizedImpExt(t *testing.T) { } func TestCleanOpenRTBRequests(t *testing.T) { + emptyTCF2Config := gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}) + testCases := []struct { req AuctionRequest bidReqAssertions func(t *testing.T, bidderRequests []BidderRequest, @@ -453,18 +446,18 @@ func TestCleanOpenRTBRequests(t *testing.T) { consentedVendors map[string]bool }{ { - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config}, bidReqAssertions: assertReq, hasError: false, applyCOPPA: true, consentedVendors: map[string]bool{"appnexus": true}, }, { - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config}, bidReqAssertions: assertReq, hasError: false, applyCOPPA: false, - consentedVendors: map[string]bool{"appnexus": true, "brightroll": true}, + consentedVendors: map[string]bool{"appnexus": true}, }, } @@ -484,16 +477,12 @@ func TestCleanOpenRTBRequests(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -515,14 +504,9 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) { App: &openrtb2.App{Name: "fpdApnApp"}, User: &openrtb2.User{Keywords: "fpdApnUser"}, } - fpd[openrtb_ext.BidderName("appnexus")] = &apnFpd + fpd[openrtb_ext.BidderName("rubicon")] = &apnFpd - brightrollFpd := firstpartydata.ResolvedFirstPartyData{ - Site: &openrtb2.Site{Name: "fpdBrightrollSite"}, - App: &openrtb2.App{Name: "fpdBrightrollApp"}, - User: &openrtb2.User{Keywords: "fpdBrightrollUser"}, - } - fpd[openrtb_ext.BidderName("brightroll")] = &brightrollFpd + emptyTCF2Config := gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}) testCases := []struct { description string @@ -531,22 +515,22 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) { }{ { description: "Pass valid FPD data for bidder not found in the request", - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd, TCF2Config: emptyTCF2Config}, fpdExpected: false, }, { description: "Pass valid FPD data for bidders specified in request", - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd, TCF2Config: emptyTCF2Config}, fpdExpected: true, }, { description: "Bidders specified in request but there is no fpd data for this bidder", - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData), TCF2Config: emptyTCF2Config}, fpdExpected: false, }, { description: "No FPD data passed", - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: nil}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: nil, TCF2Config: emptyTCF2Config}, fpdExpected: false, }, } @@ -558,16 +542,12 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -589,6 +569,47 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) { } } +func TestExtractAdapterReqBidderParamsMap(t *testing.T) { + tests := []struct { + name string + givenBidRequest *openrtb2.BidRequest + want map[string]json.RawMessage + wantErr error + }{ + { + name: "nil req", + givenBidRequest: nil, + want: nil, + wantErr: errors.New("error bidRequest should not be nil"), + }, + { + name: "nil req.ext", + givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{}}`)}, + want: nil, + wantErr: nil, + }, + { + name: "malformed req.ext", + givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage("malformed")}, + want: nil, + wantErr: errors.New("error decoding Request.ext : invalid character 'm' looking for beginning of value"), + }, + { + name: "extract bidder params from req.Ext for input request in adapter code", + givenBidRequest: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"bidderparams": {"profile": 1234, "version": 1}}}`)}, + want: map[string]json.RawMessage{"profile": json.RawMessage(`1234`), "version": json.RawMessage(`1`)}, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ExtractReqExtBidderParamsMap(tt.givenBidRequest) + assert.Equal(t, tt.wantErr, err, "err") + assert.Equal(t, tt.want, got, "result") + }) + } +} + func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { bidRespId1 := json.RawMessage(`{"id": "resp_id1"}`) bidRespId2 := json.RawMessage(`{"id": "resp_id2"}`) @@ -829,14 +850,12 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Imp: test.imps}}, UserSyncs: &emptyUsersync{}, StoredBidResponses: test.storedBidResponses, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } reqSplitter := &requestSplitter{ @@ -844,7 +863,6 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -999,6 +1017,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, Account: accountConfig, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, accountConfig.GDPR), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -1006,9 +1025,6 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, accountConfig.GDPR), - }.Builder bidderToSyncerKey := map[string]string{} reqSplitter := &requestSplitter{ @@ -1016,7 +1032,6 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { me: &metrics.MetricsEngineMock{}, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -1072,6 +1087,7 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -1079,9 +1095,6 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder privacyConfig := config.Privacy{ CCPA: config.CCPA{ @@ -1096,7 +1109,6 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { me: &metrics, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -1139,6 +1151,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -1146,9 +1159,6 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder bidderToSyncerKey := map[string]string{} metrics := metrics.MetricsEngineMock{} @@ -1158,7 +1168,6 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { me: &metrics, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -1238,6 +1247,7 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -1245,16 +1255,12 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -1312,6 +1318,7 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) { auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -1319,16 +1326,12 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -1896,6 +1899,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -1903,9 +1907,6 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder privacyConfig := config.Privacy{ LMT: config.LMT{ @@ -1918,7 +1919,6 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { me: &metrics.MetricsEngineMock{}, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -2127,6 +2127,10 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, Account: accountConfig, + TCF2Config: gdpr.NewTCF2Config( + privacyConfig.GDPR.TCF2, + accountConfig.GDPR, + ), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -2137,12 +2141,6 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { activitiesError: test.permissionsError, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config( - privacyConfig.GDPR.TCF2, - accountConfig.GDPR, - ), - }.Builder gdprDefaultValue := gdpr.SignalYes if test.gdprDefaultValue == "0" { @@ -2154,7 +2152,6 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { me: &metrics.MetricsEngineMock{}, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -2236,6 +2233,7 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, Account: accountConfig, + TCF2Config: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, accountConfig.GDPR), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -2246,9 +2244,6 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { activitiesError: nil, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, accountConfig.GDPR), - }.Builder metricsMock := metrics.MetricsEngineMock{} metricsMock.Mock.On("RecordAdapterGDPRRequestBlocked", mock.Anything).Return() @@ -2258,7 +2253,6 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { me: &metricsMock, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -2284,12 +2278,16 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { } func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) { + emptyTCF2Config := gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}) + bidReq := newBidRequest(t) bidReq.Regs = &openrtb2.Regs{} bidReq.Regs.GPP = "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN" bidReq.Regs.GPPSID = []int8{6} bidReq.User.ID = "" bidReq.User.BuyerUID = "" + bidReq.User.Yob = 0 + bidReq.User.Geo = &openrtb2.Geo{Lat: 123.46} downgradedRegs := *bidReq.Regs downgradedUser := *bidReq.User @@ -2306,14 +2304,14 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) { }{ { name: "NotSupported", - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config}, expectRegs: &downgradedRegs, expectUser: &downgradedUser, bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: false}}}, }, { name: "Supported", - req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}}, + req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config}, expectRegs: bidReq.Regs, expectUser: bidReq.User, bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: true}}}, @@ -2337,16 +2335,12 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) { allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: test.bidderInfos, } @@ -2390,14 +2384,14 @@ func TestBuildRequestExtForBidder(t *testing.T) { { description: "Prebid - Allowed Fields Only", bidderParams: nil, - requestExt: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`), - expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`), + requestExt: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), + expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), }, { description: "Prebid - Allowed Fields + Bidder Params", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, - requestExt: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`), - expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "bidderparams":"bar"}}`), + requestExt: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), + expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}, "bidderparams":"bar"}}`), }, { description: "Other", @@ -2408,8 +2402,8 @@ func TestBuildRequestExtForBidder(t *testing.T) { { description: "Prebid + Other + Bider Params", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, - requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`), - expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "bidderparams":"bar"}}`), + requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), + expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}, "bidderparams":"bar"}}`), }, { description: "Prebid + AlternateBidderCodes in pbs config but current bidder not in AlternateBidderCodes config", @@ -2557,7 +2551,7 @@ func newAdapterAliasBidRequest(t *testing.T) *openrtb2.BidRequest { Language: "EN", }, Source: &openrtb2.Source{ - TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87", + TID: "testTID", }, User: &openrtb2.User{ ID: "our-id", @@ -2578,9 +2572,9 @@ func newAdapterAliasBidRequest(t *testing.T) *openrtb2.BidRequest { H: 600, }}, }, - Ext: json.RawMessage(`{"appnexus": {"placementId": 1},"brightroll": {"placementId": 105}}`), + Ext: json.RawMessage(`{"appnexus": {"placementId": 1},"somealias": {"placementId": 105}}`), }}, - Ext: json.RawMessage(`{"prebid":{"aliases":{"brightroll":"appnexus"}}}`), + Ext: json.RawMessage(`{"prebid":{"aliases":{"somealias":"appnexus"}}}`), } } @@ -2601,13 +2595,14 @@ func newBidRequest(t *testing.T) *openrtb2.BidRequest { Language: "EN", }, Source: &openrtb2.Source{ - TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87", + TID: "testTID", }, User: &openrtb2.User{ ID: "our-id", BuyerUID: "their-id", Yob: 1982, Ext: json.RawMessage(`{}`), + Geo: &openrtb2.Geo{Lat: 123.456}, }, Imp: []openrtb2.Imp{{ ID: "some-imp-id", @@ -2620,7 +2615,7 @@ func newBidRequest(t *testing.T) *openrtb2.BidRequest { H: 600, }}, }, - Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), + Ext: json.RawMessage(`{"prebid":{"tid":"1234567", "bidder":{"appnexus": {"placementId": 1}}}}`), }}, } } @@ -2642,7 +2637,7 @@ func newBidRequestWithBidderParams(t *testing.T) *openrtb2.BidRequest { Language: "EN", }, Source: &openrtb2.Source{ - TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87", + TID: "testTID", }, User: &openrtb2.User{ ID: "our-id", @@ -3070,7 +3065,7 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { req := &openrtb2.BidRequest{ Site: &openrtb2.Site{}, Source: &openrtb2.Source{ - TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87", + TID: "testTID", }, Imp: []openrtb2.Imp{{ Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}, "axonix": { "supplyId": "123"}}}}`), @@ -3085,6 +3080,7 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ @@ -3095,16 +3091,12 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { activitiesError: nil, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -3197,17 +3189,17 @@ func Test_parseAliasesGVLIDs(t *testing.T) { "AliasGVLID Parsed Correctly", args{ orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"brightroll":"appnexus"}, "aliasgvlids":{"brightroll":1}}}`), + Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}, "aliasgvlids":{"somealiascode":1}}}`), }, }, - map[string]uint16{"brightroll": 1}, + map[string]uint16{"somealiascode": 1}, false, }, { "AliasGVLID parsing error", args{ orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"brightroll":"appnexus"}, "aliasgvlids": {"brightroll":"abc"}`), + Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}, "aliasgvlids": {"somealiascode":"abc"}`), }, }, nil, @@ -3217,7 +3209,7 @@ func Test_parseAliasesGVLIDs(t *testing.T) { "Invalid AliasGVLID", args{ orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"brightroll":"appnexus"}, "aliasgvlids":"abc"}`), + Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}, "aliasgvlids":"abc"}`), }, }, nil, @@ -3227,7 +3219,7 @@ func Test_parseAliasesGVLIDs(t *testing.T) { "Missing AliasGVLID", args{ orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"brightroll":"appnexus"}}`), + Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}}`), }, }, nil, @@ -3441,22 +3433,19 @@ func TestCleanOpenRTBRequestsFilterBidderRequestExt(t *testing.T) { BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, Account: config.Account{AlternateBidderCodes: test.inCfgABC}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ permissions: &permissionsMock{ allowAllBidders: true, }, }.Builder - tcf2ConfigBuilder := fakeTCF2ConfigBuilder{ - cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), - }.Builder reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, - tcf2ConfigBuilder: tcf2ConfigBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } @@ -3869,7 +3858,7 @@ func Test_isBidderInExtAlternateBidderCodes(t *testing.T) { name: "adapter defined in alternatebiddercodes but currentMultiBidBidder not in AllowedBidders list", args: args{ adapter: string(openrtb_ext.BidderPubmatic), - currentMultiBidBidder: string(openrtb_ext.BidderGroupm), + currentMultiBidBidder: "groupm", adapterABC: &openrtb_ext.ExtAlternateBidderCodes{ Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ string(openrtb_ext.BidderPubmatic): { @@ -3884,11 +3873,11 @@ func Test_isBidderInExtAlternateBidderCodes(t *testing.T) { name: "adapter defined in alternatebiddercodes with currentMultiBidBidder mentioned in AllowedBidders list", args: args{ adapter: string(openrtb_ext.BidderPubmatic), - currentMultiBidBidder: string(openrtb_ext.BidderGroupm), + currentMultiBidBidder: "groupm", adapterABC: &openrtb_ext.ExtAlternateBidderCodes{ Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ string(openrtb_ext.BidderPubmatic): { - AllowedBidderCodes: []string{string(openrtb_ext.BidderGroupm)}, + AllowedBidderCodes: []string{"groupm"}, }, }, }, @@ -3899,7 +3888,7 @@ func Test_isBidderInExtAlternateBidderCodes(t *testing.T) { name: "adapter defined in alternatebiddercodes with AllowedBidders list as *", args: args{ adapter: string(openrtb_ext.BidderPubmatic), - currentMultiBidBidder: string(openrtb_ext.BidderGroupm), + currentMultiBidBidder: "groupm", adapterABC: &openrtb_ext.ExtAlternateBidderCodes{ Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ string(openrtb_ext.BidderPubmatic): { @@ -4010,7 +3999,7 @@ func Test_buildRequestExtMultiBid(t *testing.T) { adapter: string(openrtb_ext.BidderPubmatic), reqMultiBid: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4025,14 +4014,14 @@ func Test_buildRequestExtMultiBid(t *testing.T) { adapterABC: &openrtb_ext.ExtAlternateBidderCodes{ Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ string(openrtb_ext.BidderPubmatic): { - AllowedBidderCodes: []string{string(openrtb_ext.BidderGroupm)}, + AllowedBidderCodes: []string{"groupm"}, }, }, }, }, want: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4047,7 +4036,7 @@ func Test_buildRequestExtMultiBid(t *testing.T) { adapter: string(openrtb_ext.BidderAppnexus), reqMultiBid: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4062,14 +4051,14 @@ func Test_buildRequestExtMultiBid(t *testing.T) { adapterABC: &openrtb_ext.ExtAlternateBidderCodes{ Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ string(openrtb_ext.BidderAppnexus): { - AllowedBidderCodes: []string{string(openrtb_ext.BidderGroupm)}, + AllowedBidderCodes: []string{"groupm"}, }, }, }, }, want: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4084,7 +4073,7 @@ func Test_buildRequestExtMultiBid(t *testing.T) { adapter: string(openrtb_ext.BidderPubmatic), reqMultiBid: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4106,7 +4095,7 @@ func Test_buildRequestExtMultiBid(t *testing.T) { }, want: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4129,7 +4118,7 @@ func Test_buildRequestExtMultiBid(t *testing.T) { adapter: string(openrtb_ext.BidderAppnexus), reqMultiBid: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4151,7 +4140,7 @@ func Test_buildRequestExtMultiBid(t *testing.T) { }, want: []*openrtb_ext.ExtMultiBid{ { - Bidder: string(openrtb_ext.BidderGroupm), + Bidder: "groupm", MaxBids: ptrutil.ToPtr(3), }, { @@ -4176,3 +4165,279 @@ func Test_buildRequestExtMultiBid(t *testing.T) { }) } } + +func TestGetPrebidMediaTypeForBid(t *testing.T) { + tests := []struct { + description string + inputBid openrtb2.Bid + expectedBidType openrtb_ext.BidType + expectedError string + }{ + { + description: "Valid bid ext with bid type native", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: json.RawMessage(`{"prebid": {"type": "native"}}`)}, + expectedBidType: openrtb_ext.BidTypeNative, + }, + { + description: "Valid bid ext with non-existing bid type", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: json.RawMessage(`{"prebid": {"type": "unknown"}}`)}, + expectedError: "Failed to parse bid mediatype for impression \"impId\", invalid BidType: unknown", + }, + { + description: "Invalid bid ext", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: json.RawMessage(`[true`)}, + expectedError: "Failed to parse bid mediatype for impression \"impId\", unexpected end of JSON input", + }, + { + description: "Bid ext is nil", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: nil}, + expectedError: "Failed to parse bid mediatype for impression \"impId\"", + }, + { + description: "Empty bid ext", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: json.RawMessage(`{}`)}, + expectedError: "Failed to parse bid mediatype for impression \"impId\"", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + bidType, err := getPrebidMediaTypeForBid(tt.inputBid) + if len(tt.expectedError) == 0 { + assert.Equal(t, tt.expectedBidType, bidType) + } else { + assert.Equal(t, tt.expectedError, err.Error()) + } + + }) + } +} + +func TestGetMediaTypeForBid(t *testing.T) { + tests := []struct { + description string + inputBid openrtb2.Bid + expectedBidType openrtb_ext.BidType + expectedError string + }{ + { + description: "Valid bid ext with bid type native", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: json.RawMessage(`{"prebid": {"type": "native"}}`)}, + expectedBidType: openrtb_ext.BidTypeNative, + }, + { + description: "invalid bid ext", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", Ext: json.RawMessage(`{"prebid"`)}, + expectedError: "Failed to parse bid mediatype for impression \"impId\", unexpected end of JSON input", + }, + { + description: "Valid bid ext with mtype native", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", MType: openrtb2.MarkupNative}, + expectedBidType: openrtb_ext.BidTypeNative, + }, + { + description: "Valid bid ext with mtype banner", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", MType: openrtb2.MarkupBanner}, + expectedBidType: openrtb_ext.BidTypeBanner, + }, + { + description: "Valid bid ext with mtype video", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", MType: openrtb2.MarkupVideo}, + expectedBidType: openrtb_ext.BidTypeVideo, + }, + { + description: "Valid bid ext with mtype audio", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", MType: openrtb2.MarkupAudio}, + expectedBidType: openrtb_ext.BidTypeAudio, + }, + { + description: "Valid bid ext with mtype unknown", + inputBid: openrtb2.Bid{ID: "bidId", ImpID: "impId", MType: 8}, + expectedError: "Failed to parse bid mType for impression \"impId\"", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + bidType, err := getMediaTypeForBid(tt.inputBid) + if len(tt.expectedError) == 0 { + assert.Equal(t, tt.expectedBidType, bidType) + } else { + assert.Equal(t, tt.expectedError, err.Error()) + } + + }) + } +} + +func TestCleanOpenRTBRequestsActivities(t *testing.T) { + testCases := []struct { + name string + req *openrtb2.BidRequest + privacyConfig config.AccountPrivacy + componentName string + allow bool + expectedReqNumber int + expectedUserYOB int64 + expectedUserLat float64 + expectedDeviceDIDMD5 string + expectedSourceTID string + }{ + { + name: "fetch_bids_request_with_one_bidder_allowed", + req: newBidRequest(t), + privacyConfig: getFetchBidsActivityConfig("appnexus", true), + expectedReqNumber: 1, + expectedUserYOB: 1982, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "testTID", + }, + { + name: "fetch_bids_request_with_one_bidder_not_allowed", + req: newBidRequest(t), + privacyConfig: getFetchBidsActivityConfig("appnexus", false), + expectedReqNumber: 0, + expectedUserYOB: 1982, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "testTID", + }, + { + name: "transmit_ufpd_allowed", + req: newBidRequest(t), + privacyConfig: getTransmitUFPDActivityConfig("appnexus", true), + expectedReqNumber: 1, + expectedUserYOB: 1982, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "testTID", + }, + { + name: "transmit_ufpd_deny", + req: newBidRequest(t), + privacyConfig: getTransmitUFPDActivityConfig("appnexus", false), + expectedReqNumber: 1, + expectedUserYOB: 0, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "", + expectedSourceTID: "testTID", + }, + { + name: "transmit_precise_geo_allowed", + req: newBidRequest(t), + privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", true), + expectedReqNumber: 1, + expectedUserYOB: 1982, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "testTID", + }, + { + name: "transmit_precise_geo_deny", + req: newBidRequest(t), + privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", false), + expectedReqNumber: 1, + expectedUserYOB: 1982, + expectedUserLat: 123.46, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "testTID", + }, + { + name: "transmit_tid_allowed", + req: newBidRequest(t), + privacyConfig: getTransmitTIDActivityConfig("appnexus", true), + expectedReqNumber: 1, + expectedUserYOB: 1982, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "testTID", + }, + { + name: "transmit_tid_deny", + req: newBidRequest(t), + privacyConfig: getTransmitTIDActivityConfig("appnexus", false), + expectedReqNumber: 1, + expectedUserYOB: 1982, + expectedUserLat: 123.456, + expectedDeviceDIDMD5: "some device ID hash", + expectedSourceTID: "", + }, + } + + for _, test := range testCases { + activities, err := privacy.NewActivityControl(&test.privacyConfig) + assert.NoError(t, err, "") + auctionReq := AuctionRequest{ + BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: test.req}, + UserSyncs: &emptyUsersync{}, + Activities: activities, + } + + bidderToSyncerKey := map[string]string{} + reqSplitter := &requestSplitter{ + bidderToSyncerKey: bidderToSyncerKey, + me: &metrics.MetricsEngineMock{}, + hostSChainNode: nil, + bidderInfo: config.BidderInfos{}, + } + + t.Run(test.name, func(t *testing.T) { + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + assert.Empty(t, errs) + assert.Len(t, bidderRequests, test.expectedReqNumber) + + if test.expectedReqNumber == 1 { + assert.Equal(t, test.expectedUserYOB, bidderRequests[0].BidRequest.User.Yob) + assert.Equal(t, test.expectedUserLat, bidderRequests[0].BidRequest.User.Geo.Lat) + assert.Equal(t, test.expectedDeviceDIDMD5, bidderRequests[0].BidRequest.Device.DIDMD5) + assert.Equal(t, test.expectedSourceTID, bidderRequests[0].BidRequest.Source.TID) + } + }) + } +} + +func buildDefaultActivityConfig(componentName string, allow bool) config.Activity { + return config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: allow, + Condition: config.ActivityCondition{ + ComponentName: []string{componentName}, + ComponentType: []string{"bidder"}, + }, + }, + }, + } +} + +func getFetchBidsActivityConfig(componentName string, allow bool) config.AccountPrivacy { + return config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + FetchBids: buildDefaultActivityConfig(componentName, allow), + }, + } +} + +func getTransmitUFPDActivityConfig(componentName string, allow bool) config.AccountPrivacy { + return config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitUserFPD: buildDefaultActivityConfig(componentName, allow), + }, + } +} + +func getTransmitPreciseGeoActivityConfig(componentName string, allow bool) config.AccountPrivacy { + return config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitPreciseGeo: buildDefaultActivityConfig(componentName, allow), + }, + } +} + +func getTransmitTIDActivityConfig(componentName string, allow bool) config.AccountPrivacy { + return config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitTids: buildDefaultActivityConfig(componentName, allow), + }, + } +} diff --git a/firstpartydata/extmerger.go b/firstpartydata/extmerger.go new file mode 100644 index 00000000000..119fa8a4c3c --- /dev/null +++ b/firstpartydata/extmerger.go @@ -0,0 +1,60 @@ +package firstpartydata + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/prebid/prebid-server/util/sliceutil" + jsonpatch "gopkg.in/evanphx/json-patch.v4" +) + +var ( + ErrBadRequest = fmt.Errorf("invalid request ext") + ErrBadFPD = fmt.Errorf("invalid first party data ext") +) + +// extMerger tracks a JSON `ext` field within an OpenRTB request. The value of the +// `ext` field is expected to be modified when calling unmarshal on the same object +// and will later be updated when invoking Merge. +type extMerger struct { + ext *json.RawMessage // Pointer to the JSON `ext` field. + snapshot json.RawMessage // Copy of the original state of the JSON `ext` field. +} + +// Track saves a copy of the JSON `ext` field and stores a reference to the extension +// object for comparison later in the Merge call. +func (e *extMerger) Track(ext *json.RawMessage) { + e.ext = ext + e.snapshot = sliceutil.Clone(*ext) +} + +// Merge applies a JSON merge of the stored extension snapshot on top of the current +// JSON of the tracked extension object. +func (e extMerger) Merge() error { + if e.ext == nil { + return nil + } + + if len(e.snapshot) == 0 { + return nil + } + + if len(*e.ext) == 0 { + *e.ext = e.snapshot + return nil + } + + merged, err := jsonpatch.MergePatch(e.snapshot, *e.ext) + if err != nil { + if errors.Is(err, jsonpatch.ErrBadJSONDoc) { + return ErrBadRequest + } else if errors.Is(err, jsonpatch.ErrBadJSONPatch) { + return ErrBadFPD + } + return err + } + + *e.ext = merged + return nil +} diff --git a/firstpartydata/extmerger_test.go b/firstpartydata/extmerger_test.go new file mode 100644 index 00000000000..784163ac313 --- /dev/null +++ b/firstpartydata/extmerger_test.go @@ -0,0 +1,109 @@ +package firstpartydata + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/util/sliceutil" + "github.com/stretchr/testify/assert" +) + +func TestExtMerger(t *testing.T) { + t.Run("nil", func(t *testing.T) { + merger := extMerger{ext: nil, snapshot: json.RawMessage(`{"a":1}`)} + assert.NoError(t, merger.Merge()) + assert.Nil(t, merger.ext) + }) + + testCases := []struct { + name string + givenOriginal json.RawMessage + givenFPD json.RawMessage + expectedExt json.RawMessage + expectedErr string + }{ + { + name: "both-populated", + givenOriginal: json.RawMessage(`{"a":1,"b":2}`), + givenFPD: json.RawMessage(`{"b":200,"c":3}`), + expectedExt: json.RawMessage(`{"a":1,"b":200,"c":3}`), + }, + { + name: "both-nil", + givenFPD: nil, + givenOriginal: nil, + expectedExt: nil, + }, + { + name: "both-empty", + givenOriginal: json.RawMessage(`{}`), + givenFPD: json.RawMessage(`{}`), + expectedExt: json.RawMessage(`{}`), + }, + { + name: "ext-nil", + givenOriginal: json.RawMessage(`{"b":2}`), + givenFPD: nil, + expectedExt: json.RawMessage(`{"b":2}`), + }, + { + name: "ext-empty", + givenOriginal: json.RawMessage(`{"b":2}`), + givenFPD: json.RawMessage(`{}`), + expectedExt: json.RawMessage(`{"b":2}`), + }, + { + name: "ext-malformed", + givenOriginal: json.RawMessage(`{"b":2}`), + givenFPD: json.RawMessage(`malformed`), + expectedErr: "invalid first party data ext", + }, + { + name: "snapshot-nil", + givenOriginal: nil, + givenFPD: json.RawMessage(`{"a":1}`), + expectedExt: json.RawMessage(`{"a":1}`), + }, + { + name: "snapshot-empty", + givenOriginal: json.RawMessage(`{}`), + givenFPD: json.RawMessage(`{"a":1}`), + expectedExt: json.RawMessage(`{"a":1}`), + }, + { + name: "snapshot-malformed", + givenOriginal: json.RawMessage(`malformed`), + givenFPD: json.RawMessage(`{"a":1}`), + expectedErr: "invalid request ext", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + // Initialize A Ext Raw Message For Testing + simulatedExt := json.RawMessage(sliceutil.Clone(test.givenOriginal)) + + // Begin Tracking + var merger extMerger + merger.Track(&simulatedExt) + + // Unmarshal + simulatedExt.UnmarshalJSON(test.givenFPD) + + // Merge + actualErr := merger.Merge() + + if test.expectedErr == "" { + assert.NoError(t, actualErr, "error") + + if test.expectedExt == nil { + assert.Nil(t, simulatedExt, "json") + } else { + assert.JSONEq(t, string(test.expectedExt), string(simulatedExt), "json") + } + } else { + assert.EqualError(t, actualErr, test.expectedErr, "error") + } + }) + } +} diff --git a/firstpartydata/first_party_data.go b/firstpartydata/first_party_data.go index 6ca7c03a883..0fde931d445 100644 --- a/firstpartydata/first_party_data.go +++ b/firstpartydata/first_party_data.go @@ -9,6 +9,8 @@ import ( "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/ortb" + "github.com/prebid/prebid-server/util/ptrutil" ) const ( @@ -16,27 +18,10 @@ const ( appKey = "app" userKey = "user" dataKey = "data" - extKey = "ext" userDataKey = "userData" appContentDataKey = "appContentData" siteContentDataKey = "siteContentData" - - keywordsKey = "keywords" - genderKey = "gender" - yobKey = "yob" - pageKey = "page" - nameKey = "name" - domainKey = "domain" - catKey = "cat" - sectionCatKey = "sectioncat" - pageCatKey = "pagecat" - searchKey = "search" - refKey = "ref" - bundleKey = "bundle" - storeUrlKey = "storeurl" - verKey = "ver" - contentKey = "content" ) type ResolvedFirstPartyData struct { @@ -47,7 +32,6 @@ type ResolvedFirstPartyData struct { // ExtractGlobalFPD extracts request level FPD from the request and removes req.{site,app,user}.ext.data if exists func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error) { - fpdReqData := make(map[string][]byte, 3) siteExt, err := req.GetSiteExt() @@ -88,7 +72,7 @@ func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error refreshExt = true } if refreshExt { - //need to keep site/app/user ext clean in case bidder is not in global fpd bidder list + // need to keep site/app/user ext clean in case bidder is not in global fpd bidder list // rebuild/resync the request in the request wrapper. if err := req.RebuildRequest(); err != nil { return nil, err @@ -100,7 +84,6 @@ func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error // ExtractOpenRtbGlobalFPD extracts and deletes user.data and {app/site}.content.data from request func ExtractOpenRtbGlobalFPD(bidRequest *openrtb2.BidRequest) map[string][]openrtb2.Data { - openRtbGlobalFPD := make(map[string][]openrtb2.Data, 3) if bidRequest.User != nil && len(bidRequest.User.Data) > 0 { openRtbGlobalFPD[userDataKey] = bidRequest.User.Data @@ -118,7 +101,6 @@ func ExtractOpenRtbGlobalFPD(bidRequest *openrtb2.BidRequest) map[string][]openr } return openRtbGlobalFPD - } // ResolveFPD consolidates First Party Data from different sources and returns valid FPD that will be applied to bidders later or returns errors @@ -130,14 +112,14 @@ func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb allBiddersTable := make(map[string]struct{}) if biddersWithGlobalFPD == nil { - //add all bidders in bidder configs to receive global data and bidder specific data + // add all bidders in bidder configs to receive global data and bidder specific data for bidderName := range fpdBidderConfigData { if _, present := allBiddersTable[string(bidderName)]; !present { allBiddersTable[string(bidderName)] = struct{}{} } } } else { - //only bidders in global bidder list will receive global data and bidder specific data + // only bidders in global bidder list will receive global data and bidder specific data for _, bidderName := range biddersWithGlobalFPD { if _, present := allBiddersTable[string(bidderName)]; !present { allBiddersTable[string(bidderName)] = struct{}{} @@ -146,7 +128,6 @@ func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb } for bidderName := range allBiddersTable { - fpdConfig := fpdBidderConfigData[openrtb_ext.BidderName(bidderName)] resolvedFpdConfig := &ResolvedFirstPartyData{} @@ -177,7 +158,7 @@ func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb } func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.User, error) { - var fpdConfigUser map[string]json.RawMessage + var fpdConfigUser json.RawMessage if fpdConfig != nil && fpdConfig.User != nil { fpdConfigUser = fpdConfig.User @@ -187,18 +168,22 @@ func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, gl return nil, nil } - newUser := openrtb2.User{} + var newUser *openrtb2.User if bidRequestUser != nil { - newUser = *bidRequestUser + newUser = ptrutil.Clone(bidRequestUser) + } else { + newUser = &openrtb2.User{} } - var err error - //apply global fpd if len(globalFPD[userKey]) > 0 { extData := buildExtData(globalFPD[userKey]) if len(newUser.Ext) > 0 { + var err error newUser.Ext, err = jsonpatch.MergePatch(newUser.Ext, extData) + if err != nil { + return nil, err + } } else { newUser.Ext = extData } @@ -207,122 +192,44 @@ func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, gl newUser.Data = openRtbGlobalFPD[userDataKey] } if fpdConfigUser != nil { - //apply bidder specific fpd if present - newUser, err = mergeUsers(&newUser, fpdConfigUser) - } - - return &newUser, err -} - -func unmarshalJSONToInt64(input json.RawMessage) (int64, error) { - var num json.Number - err := json.Unmarshal(input, &num) - if err != nil { - return -1, err - } - result, err := num.Int64() - return result, err -} - -func unmarshalJSONToString(input json.RawMessage) (string, error) { - var result string - err := json.Unmarshal(input, &result) - return result, err -} - -func unmarshalJSONToData(input json.RawMessage) ([]openrtb2.Data, error) { - var result []openrtb2.Data - err := json.Unmarshal(input, &result) - return result, err -} - -func unmarshalJSONToStringArray(input json.RawMessage) ([]string, error) { - var result []string - err := json.Unmarshal(input, &result) - return result, err -} - -func unmarshalJSONToContent(input json.RawMessage) (*openrtb2.Content, error) { - var result openrtb2.Content - err := json.Unmarshal(input, &result) - return &result, err -} - -// resolveExtension inserts remaining {site/app/user} attributes back to {site/app/user}.ext.data -func resolveExtension(fpdConfig map[string]json.RawMessage, originalExt json.RawMessage) ([]byte, error) { - resExt := originalExt - var err error - - if resExt == nil && len(fpdConfig) > 0 { - fpdExt, err := json.Marshal(fpdConfig) - return buildExtData(fpdExt), err - } - - fpdConfigExt, present := fpdConfig[extKey] - if present { - delete(fpdConfig, extKey) - resExt, err = jsonpatch.MergePatch(resExt, fpdConfigExt) - if err != nil { + if err := mergeUser(newUser, fpdConfigUser); err != nil { return nil, err } } - if len(fpdConfig) > 0 { - fpdData, err := json.Marshal(fpdConfig) - if err != nil { - return nil, err - } - data := buildExtData(fpdData) - return jsonpatch.MergePatch(resExt, data) - } - return resExt, nil + return newUser, nil } -func mergeUsers(original *openrtb2.User, fpdConfigUser map[string]json.RawMessage) (openrtb2.User, error) { +func mergeUser(v *openrtb2.User, overrideJSON json.RawMessage) error { + *v = *ortb.CloneUser(v) - var err error - newUser := *original - - if keywords, present := fpdConfigUser[keywordsKey]; present { - newUser.Keywords, err = unmarshalJSONToString(keywords) - if err != nil { - return newUser, err - } - delete(fpdConfigUser, keywordsKey) - } - if gender, present := fpdConfigUser[genderKey]; present { - newUser.Gender, err = unmarshalJSONToString(gender) - if err != nil { - return newUser, err - } - delete(fpdConfigUser, genderKey) - } - if yob, present := fpdConfigUser[yobKey]; present { - newUser.Yob, err = unmarshalJSONToInt64(yob) - if err != nil { - return newUser, err - } - delete(fpdConfigUser, yobKey) + // Track EXTs + // It's not necessary to track `ext` fields in array items because the array + // items will be replaced entirely with the override JSON, so no merge is required. + var ext, extGeo extMerger + ext.Track(&v.Ext) + if v.Geo != nil { + extGeo.Track(&v.Geo.Ext) } - if userData, present := fpdConfigUser[dataKey]; present { - newUserData, err := unmarshalJSONToData(userData) - if err != nil { - return newUser, err - } - newUser.Data = append(newUser.Data, newUserData...) - delete(fpdConfigUser, dataKey) + // Merge + if err := json.Unmarshal(overrideJSON, &v); err != nil { + return err } - if len(fpdConfigUser) > 0 { - newUser.Ext, err = resolveExtension(fpdConfigUser, original.Ext) + // Merge EXTs + if err := ext.Merge(); err != nil { + return err + } + if err := extGeo.Merge(); err != nil { + return err } - return newUser, err + return nil } func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.Site, error) { - var fpdConfigSite map[string]json.RawMessage + var fpdConfigSite json.RawMessage if fpdConfig != nil && fpdConfig.Site != nil { fpdConfigSite = fpdConfig.Site @@ -337,14 +244,22 @@ func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, gl } } - newSite := *bidRequestSite - var err error + var newSite *openrtb2.Site + if bidRequestSite != nil { + newSite = ptrutil.Clone(bidRequestSite) + } else { + newSite = &openrtb2.Site{} + } //apply global fpd if len(globalFPD[siteKey]) > 0 { extData := buildExtData(globalFPD[siteKey]) if len(newSite.Ext) > 0 { + var err error newSite.Ext, err = jsonpatch.MergePatch(newSite.Ext, extData) + if err != nil { + return nil, err + } } else { newSite.Ext = extData } @@ -359,113 +274,77 @@ func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, gl } newSite.Content.Data = openRtbGlobalFPD[siteContentDataKey] } - - newSite, err = mergeSites(&newSite, fpdConfigSite, bidderName) - return &newSite, err - + if fpdConfigSite != nil { + if err := mergeSite(newSite, fpdConfigSite, bidderName); err != nil { + return nil, err + } + } + return newSite, nil } -func mergeSites(originalSite *openrtb2.Site, fpdConfigSite map[string]json.RawMessage, bidderName string) (openrtb2.Site, error) { - var err error - newSite := *originalSite +func mergeSite(v *openrtb2.Site, overrideJSON json.RawMessage, bidderName string) error { + *v = *ortb.CloneSite(v) - if fpdConfigSite == nil { - return newSite, err + // Track EXTs + // It's not necessary to track `ext` fields in array items because the array + // items will be replaced entirely with the override JSON, so no merge is required. + var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger + ext.Track(&v.Ext) + if v.Publisher != nil { + extPublisher.Track(&v.Publisher.Ext) } - - //apply bidder specific fpd if present - if page, present := fpdConfigSite[pageKey]; present { - sitePage, err := unmarshalJSONToString(page) - if err != nil { - return newSite, err - } - //apply bidder specific fpd if present - //result site should have ID or Page, fpd becomes incorrect if it overwrites page to empty one and ID is empty in original site - if sitePage == "" && newSite.Page != "" && newSite.ID == "" { - return newSite, &errortypes.BadInput{ - Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName), - } - - } - newSite.Page = sitePage - delete(fpdConfigSite, pageKey) + if v.Content != nil { + extContent.Track(&v.Content.Ext) } - if name, present := fpdConfigSite[nameKey]; present { - newSite.Name, err = unmarshalJSONToString(name) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, nameKey) + if v.Content != nil && v.Content.Producer != nil { + extContentProducer.Track(&v.Content.Producer.Ext) } - if domain, present := fpdConfigSite[domainKey]; present { - newSite.Domain, err = unmarshalJSONToString(domain) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, domainKey) + if v.Content != nil && v.Content.Network != nil { + extContentNetwork.Track(&v.Content.Network.Ext) } - if cat, present := fpdConfigSite[catKey]; present { - newSite.Cat, err = unmarshalJSONToStringArray(cat) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, catKey) + if v.Content != nil && v.Content.Channel != nil { + extContentChannel.Track(&v.Content.Channel.Ext) } - if sectionCat, present := fpdConfigSite[sectionCatKey]; present { - newSite.SectionCat, err = unmarshalJSONToStringArray(sectionCat) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, sectionCatKey) + + // Merge + if err := json.Unmarshal(overrideJSON, &v); err != nil { + return err } - if pageCat, present := fpdConfigSite[pageCatKey]; present { - newSite.PageCat, err = unmarshalJSONToStringArray(pageCat) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, pageCatKey) + + // Merge EXTs + if err := ext.Merge(); err != nil { + return err } - if search, present := fpdConfigSite[searchKey]; present { - newSite.Search, err = unmarshalJSONToString(search) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, searchKey) + if err := extPublisher.Merge(); err != nil { + return err } - if keywords, present := fpdConfigSite[keywordsKey]; present { - newSite.Keywords, err = unmarshalJSONToString(keywords) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, keywordsKey) + if err := extContent.Merge(); err != nil { + return err } - if ref, present := fpdConfigSite[refKey]; present { - newSite.Ref, err = unmarshalJSONToString(ref) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, refKey) + if err := extContentProducer.Merge(); err != nil { + return err } - if siteContent, present := fpdConfigSite[contentKey]; present { - newSite.Content, err = mergeContents(originalSite.Content, siteContent) - if err != nil { - return newSite, err - } - delete(fpdConfigSite, contentKey) + if err := extContentNetwork.Merge(); err != nil { + return err + } + if err := extContentChannel.Merge(); err != nil { + return err } - if len(fpdConfigSite) > 0 { - newSite.Ext, err = resolveExtension(fpdConfigSite, originalSite.Ext) + // Re-Validate Site + if v.ID == "" && v.Page == "" { + return &errortypes.BadInput{ + Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName), + } } - return newSite, err + return nil } func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.App, error) { + var fpdConfigApp json.RawMessage - var fpdConfigApp map[string]json.RawMessage - - if fpdConfig != nil && fpdConfig.App != nil { + if fpdConfig != nil { fpdConfigApp = fpdConfig.App } @@ -479,14 +358,22 @@ func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globa } } - newApp := *bidRequestApp - var err error + var newApp *openrtb2.App + if bidRequestApp != nil { + newApp = ptrutil.Clone(bidRequestApp) + } else { + newApp = &openrtb2.App{} + } //apply global fpd if exists if len(globalFPD[appKey]) > 0 { extData := buildExtData(globalFPD[appKey]) if len(newApp.Ext) > 0 { + var err error newApp.Ext, err = jsonpatch.MergePatch(newApp.Ext, extData) + if err != nil { + return nil, err + } } else { newApp.Ext = extData } @@ -503,100 +390,69 @@ func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globa newApp.Content.Data = openRtbGlobalFPD[appContentDataKey] } - newApp, err = mergeApps(&newApp, fpdConfigApp) + if fpdConfigApp != nil { + if err := mergeApp(newApp, fpdConfigApp); err != nil { + return nil, err + } + } - return &newApp, err + return newApp, nil } -func mergeApps(originalApp *openrtb2.App, fpdConfigApp map[string]json.RawMessage) (openrtb2.App, error) { - - var err error - newApp := *originalApp +func mergeApp(v *openrtb2.App, overrideJSON json.RawMessage) error { + *v = *ortb.CloneApp(v) - if fpdConfigApp == nil { - return newApp, err + // Track EXTs + // It's not necessary to track `ext` fields in array items because the array + // items will be replaced entirely with the override JSON, so no merge is required. + var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger + ext.Track(&v.Ext) + if v.Publisher != nil { + extPublisher.Track(&v.Publisher.Ext) } - //apply bidder specific fpd if present - if name, present := fpdConfigApp[nameKey]; present { - newApp.Name, err = unmarshalJSONToString(name) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, nameKey) + if v.Content != nil { + extContent.Track(&v.Content.Ext) } - if bundle, present := fpdConfigApp[bundleKey]; present { - newApp.Bundle, err = unmarshalJSONToString(bundle) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, bundleKey) + if v.Content != nil && v.Content.Producer != nil { + extContentProducer.Track(&v.Content.Producer.Ext) } - if domain, present := fpdConfigApp[domainKey]; present { - newApp.Domain, err = unmarshalJSONToString(domain) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, domainKey) + if v.Content != nil && v.Content.Network != nil { + extContentNetwork.Track(&v.Content.Network.Ext) } - if storeUrl, present := fpdConfigApp[storeUrlKey]; present { - newApp.StoreURL, err = unmarshalJSONToString(storeUrl) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, storeUrlKey) + if v.Content != nil && v.Content.Channel != nil { + extContentChannel.Track(&v.Content.Channel.Ext) } - if cat, present := fpdConfigApp[catKey]; present { - newApp.Cat, err = unmarshalJSONToStringArray(cat) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, catKey) + + // Merge + if err := json.Unmarshal(overrideJSON, &v); err != nil { + return err } - if sectionCat, present := fpdConfigApp[sectionCatKey]; present { - newApp.SectionCat, err = unmarshalJSONToStringArray(sectionCat) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, sectionCatKey) + + // Merge EXTs + if err := ext.Merge(); err != nil { + return err } - if pageCat, present := fpdConfigApp[pageCatKey]; present { - newApp.PageCat, err = unmarshalJSONToStringArray(pageCat) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, pageCatKey) + if err := extPublisher.Merge(); err != nil { + return err } - if version, present := fpdConfigApp[verKey]; present { - newApp.Ver, err = unmarshalJSONToString(version) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, verKey) + if err := extContent.Merge(); err != nil { + return err } - if keywords, present := fpdConfigApp[keywordsKey]; present { - newApp.Keywords, err = unmarshalJSONToString(keywords) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, keywordsKey) + if err := extContentProducer.Merge(); err != nil { + return err } - if appContent, present := fpdConfigApp[contentKey]; present { - newApp.Content, err = mergeContents(originalApp.Content, appContent) - if err != nil { - return newApp, err - } - delete(fpdConfigApp, contentKey) + if err := extContentNetwork.Merge(); err != nil { + return err } - - if len(fpdConfigApp) > 0 { - newApp.Ext, err = resolveExtension(fpdConfigApp, originalApp.Ext) + if err := extContentChannel.Merge(); err != nil { + return err } - return newApp, err + return nil } func buildExtData(data []byte) []byte { - res := make([]byte, 0, len(data)) + res := make([]byte, 0, len(data)+len(`"{"data":}"`)) res = append(res, []byte(`{"data":`)...) res = append(res, data...) res = append(res, []byte(`}`)...) @@ -605,7 +461,6 @@ func buildExtData(data []byte) []byte { // ExtractBidderConfigFPD extracts bidder specific configs from req.ext.prebid.bidderconfig func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, error) { - fpd := make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2) reqExtPrebid := reqExt.GetPrebid() if reqExtPrebid != nil { @@ -639,12 +494,10 @@ func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.Bid reqExt.SetPrebid(reqExtPrebid) } return fpd, nil - } // ExtractFPDForBidders extracts FPD data from request if specified func ExtractFPDForBidders(req *openrtb_ext.RequestWrapper) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) { - reqExt, err := req.GetRequestExt() if err != nil { return nil, []error{err} @@ -680,24 +533,4 @@ func ExtractFPDForBidders(req *openrtb_ext.RequestWrapper) (map[openrtb_ext.Bidd } return ResolveFPD(req.BidRequest, fbdBidderConfigData, globalFpd, openRtbGlobalFPD, biddersWithGlobalFPD) - -} - -func mergeContents(originalContent *openrtb2.Content, fpdBidderConfigContent json.RawMessage) (*openrtb2.Content, error) { - if originalContent == nil { - return unmarshalJSONToContent(fpdBidderConfigContent) - } - originalContentBytes, err := json.Marshal(originalContent) - if err != nil { - return nil, err - } - newFinalContentBytes, err := jsonpatch.MergePatch(originalContentBytes, fpdBidderConfigContent) - if err != nil { - return nil, err - } - newFinalContent, err := unmarshalJSONToContent(newFinalContentBytes) - if err != nil { - return nil, err - } - return newFinalContent, nil } diff --git a/firstpartydata/first_party_data_test.go b/firstpartydata/first_party_data_test.go index 2712e79f244..4c9cd7ad5e8 100644 --- a/firstpartydata/first_party_data_test.go +++ b/firstpartydata/first_party_data_test.go @@ -3,16 +3,17 @@ package firstpartydata import ( "encoding/json" "os" + "reflect" "testing" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExtractGlobalFPD(t *testing.T) { - testCases := []struct { description string input openrtb_ext.RequestWrapper @@ -255,7 +256,6 @@ func TestExtractGlobalFPD(t *testing.T) { } func TestExtractOpenRtbGlobalFPD(t *testing.T) { - testCases := []struct { description string input openrtb2.BidRequest @@ -467,100 +467,86 @@ func TestExtractOpenRtbGlobalFPD(t *testing.T) { } func TestExtractBidderConfigFPD(t *testing.T) { + testPath := "tests/extractbidderconfigfpd" - if specFiles, err := os.ReadDir("./tests/extractbidderconfigfpd"); err == nil { - for _, specFile := range specFiles { - fileName := "./tests/extractbidderconfigfpd/" + specFile.Name() + tests, err := os.ReadDir(testPath) + require.NoError(t, err, "Cannot Discover Tests") - fpdFile, err := loadFpdFile(fileName) - if err != nil { - t.Errorf("Unable to load file: %s", fileName) - } - var extReq openrtb_ext.ExtRequestPrebid - err = json.Unmarshal(fpdFile.InputRequestData, &extReq) - if err != nil { - t.Errorf("Unable to unmarshal input request: %s", fileName) - } - reqExt := openrtb_ext.RequestExt{} - reqExt.SetPrebid(&extReq) - fpdData, err := ExtractBidderConfigFPD(&reqExt) + for _, test := range tests { + t.Run(test.Name(), func(t *testing.T) { + filePath := testPath + "/" + test.Name() - if len(fpdFile.ValidationErrors) > 0 { - assert.Equal(t, err.Error(), fpdFile.ValidationErrors[0].Message, "Incorrect first party data error message") - continue - } + fpdFile, err := loadFpdFile(filePath) + require.NoError(t, err, "Cannot Load Test") - assert.Nil(t, reqExt.GetPrebid().BidderConfigs, "Bidder specific FPD config should be removed from request") + givenRequestExtPrebid := &openrtb_ext.ExtRequestPrebid{} + err = json.Unmarshal(fpdFile.InputRequestData, givenRequestExtPrebid) + require.NoError(t, err, "Cannot Load Test Conditions") - assert.Nil(t, err, "No error should be returned") - assert.Equal(t, len(fpdFile.BidderConfigFPD), len(fpdData), "Incorrect fpd data") + testRequest := &openrtb_ext.RequestExt{} + testRequest.SetPrebid(givenRequestExtPrebid) - for bidderName, bidderFPD := range fpdFile.BidderConfigFPD { + // run test + results, err := ExtractBidderConfigFPD(testRequest) - if bidderFPD.Site != nil { - resSite := fpdData[bidderName].Site - for k, v := range bidderFPD.Site { - assert.NotNil(t, resSite[k], "Property is not found in result site") - assert.JSONEq(t, string(v), string(resSite[k]), "site is incorrect") - } + // assert errors + if len(fpdFile.ValidationErrors) > 0 { + require.EqualError(t, err, fpdFile.ValidationErrors[0].Message, "Expected Error Not Received") + } else { + require.NoError(t, err, "Error Not Expected") + assert.Nil(t, testRequest.GetPrebid().BidderConfigs, "Bidder specific FPD config should be removed from request") + } + + // assert fpd (with normalization for nicer looking tests) + for bidderName, expectedFPD := range fpdFile.BidderConfigFPD { + if expectedFPD.App != nil { + assert.JSONEq(t, string(expectedFPD.App), string(results[bidderName].App), "app is incorrect") } else { - assert.Nil(t, fpdData[bidderName].Site, "Result site should be also nil") + assert.Nil(t, results[bidderName].App, "app expected to be nil") } - if bidderFPD.App != nil { - resApp := fpdData[bidderName].App - for k, v := range bidderFPD.App { - assert.NotNil(t, resApp[k], "Property is not found in result app") - assert.JSONEq(t, string(v), string(resApp[k]), "app is incorrect") - } + if expectedFPD.Site != nil { + assert.JSONEq(t, string(expectedFPD.Site), string(results[bidderName].Site), "site is incorrect") } else { - assert.Nil(t, fpdData[bidderName].App, "Result app should be also nil") + assert.Nil(t, results[bidderName].Site, "site expected to be nil") } - if bidderFPD.User != nil { - resUser := fpdData[bidderName].User - for k, v := range bidderFPD.User { - assert.NotNil(t, resUser[k], "Property is not found in result user") - assert.JSONEq(t, string(v), string(resUser[k]), "site is incorrect") - } + if expectedFPD.User != nil { + assert.JSONEq(t, string(expectedFPD.User), string(results[bidderName].User), "user is incorrect") } else { - assert.Nil(t, fpdData[bidderName].User, "Result user should be also nil") + assert.Nil(t, results[bidderName].User, "user expected to be nil") } } - } + }) } } func TestResolveFPD(t *testing.T) { + testPath := "tests/resolvefpd" - if specFiles, err := os.ReadDir("./tests/resolvefpd"); err == nil { - for _, specFile := range specFiles { - fileName := "./tests/resolvefpd/" + specFile.Name() + tests, err := os.ReadDir(testPath) + require.NoError(t, err, "Cannot Discover Tests") - fpdFile, err := loadFpdFile(fileName) - if err != nil { - t.Errorf("Unable to load file: %s", fileName) - } + for _, test := range tests { + t.Run(test.Name(), func(t *testing.T) { + filePath := testPath + "/" + test.Name() - var inputReq openrtb2.BidRequest - err = json.Unmarshal(fpdFile.InputRequestData, &inputReq) - if err != nil { - t.Errorf("Unable to unmarshal input request: %s", fileName) - } + fpdFile, err := loadFpdFile(filePath) + require.NoError(t, err, "Cannot Load Test") - var inputReqCopy openrtb2.BidRequest - err = json.Unmarshal(fpdFile.InputRequestData, &inputReqCopy) - if err != nil { - t.Errorf("Unable to unmarshal input request: %s", fileName) - } + request := &openrtb2.BidRequest{} + err = json.Unmarshal(fpdFile.InputRequestData, &request) + require.NoError(t, err, "Cannot Load Request") - var outputReq openrtb2.BidRequest + originalRequest := &openrtb2.BidRequest{} + err = json.Unmarshal(fpdFile.InputRequestData, &originalRequest) + require.NoError(t, err, "Cannot Load Request") + + outputReq := &openrtb2.BidRequest{} err = json.Unmarshal(fpdFile.OutputRequestData, &outputReq) - if err != nil { - t.Errorf("Unable to unmarshal output request: %s", fileName) - } + require.NoError(t, err, "Cannot Load Output Request") - reqExtFPD := make(map[string][]byte, 3) + reqExtFPD := make(map[string][]byte) reqExtFPD["site"] = fpdFile.GlobalFPD["site"] reqExtFPD["app"] = fpdFile.GlobalFPD["app"] reqExtFPD["user"] = fpdFile.GlobalFPD["user"] @@ -572,7 +558,7 @@ func TestResolveFPD(t *testing.T) { var siteConData []openrtb2.Data err = json.Unmarshal(reqFPDSiteContentData, &siteConData) if err != nil { - t.Errorf("Unable to unmarshal site.content.data: %s", fileName) + t.Errorf("Unable to unmarshal site.content.data:") } reqFPD[siteContentDataKey] = siteConData } @@ -582,7 +568,7 @@ func TestResolveFPD(t *testing.T) { var appConData []openrtb2.Data err = json.Unmarshal(reqFPDAppContentData, &appConData) if err != nil { - t.Errorf("Unable to unmarshal app.content.data: %s", fileName) + t.Errorf("Unable to unmarshal app.content.data: ") } reqFPD[appContentDataKey] = appConData } @@ -592,7 +578,7 @@ func TestResolveFPD(t *testing.T) { var userData []openrtb2.Data err = json.Unmarshal(reqFPDUserData, &userData) if err != nil { - t.Errorf("Unable to unmarshal app.content.data: %s", fileName) + t.Errorf("Unable to unmarshal app.content.data: ") } reqFPD[userDataKey] = userData } @@ -601,10 +587,11 @@ func TestResolveFPD(t *testing.T) { fpdFile.BidderConfigFPD["appnexus"] = &openrtb_ext.ORTB2{} } - resultFPD, errL := ResolveFPD(&inputReq, fpdFile.BidderConfigFPD, reqExtFPD, reqFPD, []string{"appnexus"}) + // run test + resultFPD, errL := ResolveFPD(request, fpdFile.BidderConfigFPD, reqExtFPD, reqFPD, []string{"appnexus"}) if len(errL) == 0 { - assert.Equal(t, inputReq, inputReqCopy, "Original request should not be modified") + assert.Equal(t, request, originalRequest, "Original request should not be modified") bidderFPD := resultFPD["appnexus"] @@ -622,6 +609,7 @@ func TestResolveFPD(t *testing.T) { expectedAppExt := outputReq.App.Ext bidderFPD.App.Ext = nil outputReq.App.Ext = nil + assert.JSONEq(t, string(expectedAppExt), string(resAppExt), "app.ext is incorrect") assert.Equal(t, outputReq.App, bidderFPD.App, "App is incorrect") @@ -638,13 +626,11 @@ func TestResolveFPD(t *testing.T) { } else { assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect first party data warning message") } - - } + }) } } func TestExtractFPDForBidders(t *testing.T) { - if specFiles, err := os.ReadDir("./tests/extractfpdforbidders"); err == nil { for _, specFile := range specFiles { fileName := "./tests/extractfpdforbidders/" + specFile.Name() @@ -758,663 +744,1186 @@ type fpdFile struct { } func TestResolveUser(t *testing.T) { - - fpdConfigUser := make(map[string]json.RawMessage, 0) - fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`) - fpdConfigUser[yobKey] = []byte(`1980`) - fpdConfigUser[genderKey] = []byte(`"M"`) - fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`) - fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`) - fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`) - - bidRequestUser := &openrtb2.User{ - ID: "bidRequestUserId", - Yob: 1990, - Gender: "F", - Keywords: "bidRequestUserKeywords", - } - - globalFPD := make(map[string][]byte, 0) - globalFPD[userKey] = []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`) - - openRtbGlobalFPD := make(map[string][]openrtb2.Data, 0) - openRtbGlobalFPD[userDataKey] = []openrtb2.Data{ - {ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"}, - {ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"}, + testCases := []struct { + description string + fpdConfig *openrtb_ext.ORTB2 + bidRequestUser *openrtb2.User + globalFPD map[string][]byte + openRtbGlobalFPD map[string][]openrtb2.Data + expectedUser *openrtb2.User + expectedError string + }{ + { + description: "FPD config and bid request user are not specified", + expectedUser: nil, + }, + { + description: "FPD config user only is specified", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test"}`)}, + expectedUser: &openrtb2.User{ID: "test"}, + }, + { + description: "FPD config and bid request user are specified", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, + bidRequestUser: &openrtb2.User{ID: "test2"}, + expectedUser: &openrtb2.User{ID: "test1"}, + }, + { + description: "FPD config, bid request and global fpd user are specified, no input user ext", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, + bidRequestUser: &openrtb2.User{ID: "test2"}, + globalFPD: map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)}, + expectedUser: &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue"}}`)}, + }, + { + description: "FPD config, bid request user with ext and global fpd user are specified, no input user ext", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, + bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, + globalFPD: map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)}, + expectedUser: &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue"},"test":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, + }, + { + description: "FPD config, bid request and global fpd user are specified, with input user ext.data", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, + bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, + globalFPD: map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)}, + expectedUser: &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue","inputFPDUserData":"inputFPDUserDataValue"}}`)}, + }, + { + description: "FPD config, bid request and global fpd user are specified, with input user ext.data malformed", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, + bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, + globalFPD: map[string][]byte{userKey: []byte(`malformed`)}, + expectedError: "Invalid JSON Patch", + }, + { + description: "bid request and openrtb global fpd user are specified, no input user ext", + bidRequestUser: &openrtb2.User{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedUser: &openrtb2.User{ID: "test2", Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + }, + { + description: "fpd config user, bid request and openrtb global fpd user are specified, no input user ext", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, + bidRequestUser: &openrtb2.User{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + }, + { + description: "fpd config user with ext, bid request and openrtb global fpd user are specified, no input user ext", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, + bidRequestUser: &openrtb2.User{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }, + Ext: json.RawMessage(`{"test":1}`)}, + }, + { + description: "fpd config user with ext, bid requestuser with ext and openrtb global fpd user are specified, no input user ext", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, + bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, + openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }, + Ext: json.RawMessage(`{"key":"value","test":1}`)}, + }, + { + description: "fpd config user with malformed ext, bid requestuser with ext and openrtb global fpd user are specified, no input user ext", + fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)}, + bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, + openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }, + Ext: json.RawMessage(`{"key":"value","test":1}`), + }, + expectedError: "invalid character 'm' looking for beginning of object key string", + }, } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + resultUser, err := resolveUser(test.fpdConfig, test.bidRequestUser, test.globalFPD, test.openRtbGlobalFPD, "bidderA") - expectedUser := &openrtb2.User{ - ID: "bidRequestUserId", - Yob: 1980, - Gender: "M", - Keywords: "fpdConfigUserKeywords", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"}, - {ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"}, - {ID: "UserDataId1", Name: "UserDataName1"}, - {ID: "UserDataId2", Name: "UserDataName2"}, - }, + if test.expectedError == "" { + assert.NoError(t, err, "unexpected error returned") + assert.Equal(t, test.expectedUser, resultUser, "Result user is incorrect") + } else { + assert.EqualError(t, err, test.expectedError, "expected error incorrect") + } + }) } +} +func TestResolveSite(t *testing.T) { testCases := []struct { - description string - bidRequestUserExt []byte - expectedUserExt string + description string + fpdConfig *openrtb_ext.ORTB2 + bidRequestSite *openrtb2.Site + globalFPD map[string][]byte + openRtbGlobalFPD map[string][]openrtb2.Data + expectedSite *openrtb2.Site + expectedError string }{ { - description: "bid request user.ext is nil", - bidRequestUserExt: nil, - expectedUserExt: `{"data":{ - "fpdConfigUserExt":123, - "globalFPDUserData":"globalFPDUserDataValue", - "id":"fpdConfigUserId" - } - }`, - }, - { - description: "bid request user.ext is not nil", - bidRequestUserExt: []byte(`{"bidRequestUserExt": 1234}`), - expectedUserExt: `{"data":{ - "fpdConfigUserExt":123, - "globalFPDUserData":"globalFPDUserDataValue", - "id":"fpdConfigUserId" - }, - "bidRequestUserExt":1234 - }`, + description: "FPD config and bid request site are not specified", + expectedSite: nil, + }, + { + description: "FPD config site only is specified", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test"}`)}, + expectedError: "incorrect First Party Data for bidder bidderA: Site object is not defined in request, but defined in FPD config", + }, + { + description: "FPD config and bid request site are specified", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2"}, + expectedSite: &openrtb2.Site{ID: "test1"}, + }, + { + description: "FPD config, bid request and global fpd site are specified, no input site ext", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2"}, + globalFPD: map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)}, + expectedSite: &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue"}}`)}, + }, + { + description: "FPD config, bid request site with ext and global fpd site are specified, no input site ext", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, + globalFPD: map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)}, + expectedSite: &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue"},"test":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, + }, + { + description: "FPD config, bid request and global fpd site are specified, with input site ext.data", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, + globalFPD: map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)}, + expectedSite: &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue","inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, + }, + { + description: "FPD config, bid request and global fpd site are specified, with input site ext.data malformed", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, + globalFPD: map[string][]byte{siteKey: []byte(`malformed`)}, + expectedError: "Invalid JSON Patch", + }, + { + description: "bid request and openrtb global fpd site are specified, no input site ext", + bidRequestSite: &openrtb2.Site{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}}, + }, + { + description: "bid request with content and openrtb global fpd site are specified, no input site ext", + bidRequestSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{ + ID: "InputSiteContentId", + Data: []openrtb2.Data{ + {ID: "1", Name: "N1"}, + {ID: "2", Name: "N2"}, + }, + Ext: json.RawMessage(`{"contentPresent":true}`), + }}, + openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{ + ID: "InputSiteContentId", + Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }, + Ext: json.RawMessage(`{"contentPresent":true}`), + }}, + }, + { + description: "fpd config site, bid request and openrtb global fpd site are specified, no input site ext", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}}, + }, + { + description: "fpd config site with ext, bid request and openrtb global fpd site are specified, no input site ext", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + Ext: json.RawMessage(`{"test":1}`)}, + }, + { + description: "fpd config site with ext, bid request site with ext and openrtb global fpd site are specified, no input site ext", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, + openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + Ext: json.RawMessage(`{"key":"value","test":1}`)}, + }, + { + description: "fpd config site with malformed ext, bid request site with ext and openrtb global fpd site are specified, no input site ext", + fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)}, + bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, + openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + Ext: json.RawMessage(`{"key":"value","test":1}`), + }, + expectedError: "invalid character 'm' looking for beginning of object key string", }, } - for _, test := range testCases { - bidRequestUser.Ext = test.bidRequestUserExt - - fpdConfigUser := make(map[string]json.RawMessage, 0) - fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`) - fpdConfigUser[yobKey] = []byte(`1980`) - fpdConfigUser[genderKey] = []byte(`"M"`) - fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`) - fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`) - fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`) - fpdConfig := &openrtb_ext.ORTB2{User: fpdConfigUser} - - resultUser, err := resolveUser(fpdConfig, bidRequestUser, globalFPD, openRtbGlobalFPD, "appnexus") - assert.NoError(t, err, "No error should be returned") - - assert.JSONEq(t, test.expectedUserExt, string(resultUser.Ext), "Result user.Ext is incorrect") - resultUser.Ext = nil - assert.Equal(t, expectedUser, resultUser, "Result user is incorrect") - } - -} + t.Run(test.description, func(t *testing.T) { + resultSite, err := resolveSite(test.fpdConfig, test.bidRequestSite, test.globalFPD, test.openRtbGlobalFPD, "bidderA") -func TestResolveUserNilValues(t *testing.T) { - resultUser, err := resolveUser(nil, nil, nil, nil, "appnexus") - assert.NoError(t, err, "No error should be returned") - assert.Nil(t, resultUser, "Result user should be nil") + if test.expectedError == "" { + assert.NoError(t, err, "unexpected error returned") + assert.Equal(t, test.expectedSite, resultSite, "Result site is incorrect") + } else { + assert.EqualError(t, err, test.expectedError, "expected error incorrect") + } + }) + } } -func TestMergeUsers(t *testing.T) { - - originalUser := &openrtb2.User{ - ID: "bidRequestUserId", - Yob: 1980, - Gender: "M", - Keywords: "fpdConfigUserKeywords", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"}, - {ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"}, +func TestResolveApp(t *testing.T) { + testCases := []struct { + description string + fpdConfig *openrtb_ext.ORTB2 + bidRequestApp *openrtb2.App + globalFPD map[string][]byte + openRtbGlobalFPD map[string][]openrtb2.Data + expectedApp *openrtb2.App + expectedError string + }{ + { + description: "FPD config and bid request app are not specified", + expectedApp: nil, }, - Ext: []byte(`{"bidRequestUserExt": 1234}`), - } - fpdConfigUser := make(map[string]json.RawMessage, 0) - fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`) - fpdConfigUser[yobKey] = []byte(`1980`) - fpdConfigUser[genderKey] = []byte(`"M"`) - fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`) - fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`) - fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`) - - resultUser, err := mergeUsers(originalUser, fpdConfigUser) - assert.NoError(t, err, "No error should be returned") - - expectedUserExt := `{"bidRequestUserExt":1234, - "data":{ - "fpdConfigUserExt":123, - "id":"fpdConfigUserId"} - }` - assert.JSONEq(t, expectedUserExt, string(resultUser.Ext), "Result user.Ext is incorrect") - resultUser.Ext = nil - - expectedUser := openrtb2.User{ - ID: "bidRequestUserId", - Yob: 1980, - Gender: "M", - Keywords: "fpdConfigUserKeywords", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"}, - {ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"}, - {ID: "UserDataId1", Name: "UserDataName1"}, - {ID: "UserDataId2", Name: "UserDataName2"}, + { + description: "FPD config app only is specified", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test"}`)}, + expectedError: "incorrect First Party Data for bidder bidderA: App object is not defined in request, but defined in FPD config", + }, + { + description: "FPD config and bid request app are specified", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, + bidRequestApp: &openrtb2.App{ID: "test2"}, + expectedApp: &openrtb2.App{ID: "test1"}, + }, + { + description: "FPD config, bid request and global fpd app are specified, no input app ext", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, + bidRequestApp: &openrtb2.App{ID: "test2"}, + globalFPD: map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)}, + expectedApp: &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue"}}`)}, + }, + { + description: "FPD config, bid request app with ext and global fpd app are specified, no input app ext", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, + bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, + globalFPD: map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)}, + expectedApp: &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue"},"test":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, + }, + { + description: "FPD config, bid request and global fpd app are specified, with input app ext.data", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, + bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, + globalFPD: map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)}, + expectedApp: &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue","inputFPDAppData":"inputFPDAppDataValue"}}`)}, + }, + { + description: "FPD config, bid request and global fpd app are specified, with input app ext.data malformed", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, + bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, + globalFPD: map[string][]byte{appKey: []byte(`malformed`)}, + expectedError: "Invalid JSON Patch", + }, + { + description: "bid request and openrtb global fpd app are specified, no input app ext", + bidRequestApp: &openrtb2.App{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}}, + }, + { + description: "bid request with content and openrtb global fpd app are specified, no input app ext", + bidRequestApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{ + ID: "InputAppContentId", + Data: []openrtb2.Data{ + {ID: "1", Name: "N1"}, + {ID: "2", Name: "N2"}, + }, + Ext: json.RawMessage(`{"contentPresent":true}`), + }}, + openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{ + ID: "InputAppContentId", + Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }, + Ext: json.RawMessage(`{"contentPresent":true}`), + }}, + }, + { + description: "fpd config app, bid request and openrtb global fpd app are specified, no input app ext", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, + bidRequestApp: &openrtb2.App{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}}, + }, + { + description: "fpd config app with ext, bid request and openrtb global fpd app are specified, no input app ext", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, + bidRequestApp: &openrtb2.App{ID: "test2"}, + openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + Ext: json.RawMessage(`{"test":1}`)}, + }, + { + description: "fpd config app with ext, bid request app with ext and openrtb global fpd app are specified, no input app ext", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, + bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, + openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + Ext: json.RawMessage(`{"key":"value","test":1}`)}, + }, + { + description: "fpd config app with malformed ext, bid request app with ext and openrtb global fpd app are specified, no input app ext", + fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)}, + bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, + openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ + {ID: "DataId1", Name: "Name1"}, + {ID: "DataId2", Name: "Name2"}, + }}, + Ext: json.RawMessage(`{"key":"value","test":1}`), + }, + expectedError: "invalid character 'm' looking for beginning of object key string", }, } - assert.Equal(t, expectedUser, resultUser, "Result user is incorrect") -} + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + resultApp, err := resolveApp(test.fpdConfig, test.bidRequestApp, test.globalFPD, test.openRtbGlobalFPD, "bidderA") -func TestResolveExtension(t *testing.T) { + if test.expectedError == "" { + assert.NoError(t, err, "unexpected error returned") + assert.Equal(t, test.expectedApp, resultApp, "Result app is incorrect") + } else { + assert.EqualError(t, err, test.expectedError, "expected error incorrect") + } + }) + } +} +func TestBuildExtData(t *testing.T) { testCases := []struct { description string - fpdConfig map[string]json.RawMessage - originalExt json.RawMessage - expectedExt string + input []byte + expectedRes string }{ - {description: "Fpd config with ext only", - fpdConfig: map[string]json.RawMessage{"ext": json.RawMessage(`{"data":{"fpdConfigUserExt": 123}}`)}, - originalExt: json.RawMessage(`{"bidRequestUserExt": 1234}`), - expectedExt: `{"bidRequestUserExt":1234, "data":{"fpdConfigUserExt":123}}`, - }, - {description: "Fpd config with ext and another property", - fpdConfig: map[string]json.RawMessage{"ext": json.RawMessage(`{"data":{"fpdConfigUserExt": 123}}`), "prebid": json.RawMessage(`{"prebidData":{"isPrebid": true}}`)}, - originalExt: json.RawMessage(`{"bidRequestUserExt": 1234}`), - expectedExt: `{"bidRequestUserExt":1234, "data":{"fpdConfigUserExt":123, "prebid":{"prebidData":{"isPrebid": true}}}}`, - }, - {description: "Fpd config empty", - fpdConfig: nil, - originalExt: json.RawMessage(`{"bidRequestUserExt": 1234}`), - expectedExt: `{"bidRequestUserExt":1234}`, - }, - {description: "Original ext empty", - fpdConfig: map[string]json.RawMessage{"ext": json.RawMessage(`{"data":{"fpdConfigUserExt": 123}}`)}, - originalExt: nil, - expectedExt: `{"data":{"ext":{"data":{"fpdConfigUserExt":123}}}}`, + { + description: "Input object with int value", + input: []byte(`{"someData": 123}`), + expectedRes: `{"data": {"someData": 123}}`, + }, + { + description: "Input object with bool value", + input: []byte(`{"someData": true}`), + expectedRes: `{"data": {"someData": true}}`, + }, + { + description: "Input object with string value", + input: []byte(`{"someData": "true"}`), + expectedRes: `{"data": {"someData": "true"}}`, + }, + { + description: "No input object", + input: []byte(`{}`), + expectedRes: `{"data": {}}`, + }, + { + description: "Input object with object value", + input: []byte(`{"someData": {"moreFpdData": "fpddata"}}`), + expectedRes: `{"data": {"someData": {"moreFpdData": "fpddata"}}}`, }, } for _, test := range testCases { - resExt, err := resolveExtension(test.fpdConfig, test.originalExt) - assert.NoError(t, err, "No error should be returned") - assert.JSONEq(t, test.expectedExt, string(resExt), "result ext is incorrect") + actualRes := buildExtData(test.input) + assert.JSONEq(t, test.expectedRes, string(actualRes), "Incorrect result data") } } -func TestResolveSite(t *testing.T) { - - fpdConfigSite := make(map[string]json.RawMessage, 0) - fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`) - fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`) - fpdConfigSite[nameKey] = []byte(`"fpdConfigSiteName"`) - fpdConfigSite[pageKey] = []byte(`"fpdConfigSitePage"`) - fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`) - fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`) - - bidRequestSite := &openrtb2.Site{ - ID: "bidRequestSiteId", - Keywords: "bidRequestSiteKeywords", - Name: "bidRequestSiteName", - Page: "bidRequestSitePage", - Content: &openrtb2.Content{ - ID: "bidRequestSiteContentId", - Episode: 4, - Data: []openrtb2.Data{ - {ID: "bidRequestSiteContentDataId1", Name: "bidRequestSiteContentDataName1"}, - {ID: "bidRequestSiteContentDataId2", Name: "bidRequestSiteContentDataName2"}, - }, +func TestMergeUser(t *testing.T) { + testCases := []struct { + name string + givenUser openrtb2.User + givenFPD json.RawMessage + expectedUser openrtb2.User + expectedErr string + }{ + { + name: "empty", + givenUser: openrtb2.User{}, + givenFPD: []byte(`{}`), + expectedUser: openrtb2.User{}, + }, + { + name: "toplevel", + givenUser: openrtb2.User{ID: "1"}, + givenFPD: []byte(`{"id":"2"}`), + expectedUser: openrtb2.User{ID: "2"}, + }, + { + name: "toplevel-ext", + givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`)}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), + expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, + }, + { + name: "toplevel-ext-err", + givenUser: openrtb2.User{ID: "1", Ext: []byte(`malformed`)}, + givenFPD: []byte(`{"id":"2"}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-geo", + givenUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: 1}}, + givenFPD: []byte(`{"geo":{"lat": 2}}`), + expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: 2}}, + }, + { + name: "nested-geo-ext", + givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":2}`)}}, + givenFPD: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), + expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-geo-ext", + givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":20}`)}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "geo":{"ext":{"b":100,"c":3}}}`), + expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "nested-geo-ext-err", + givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`malformed`)}}, + givenFPD: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), + expectedErr: "invalid request ext", + }, + { + name: "fpd-err", + givenUser: openrtb2.User{ID: "1", Ext: []byte(`{"a":1}`)}, + givenFPD: []byte(`malformed`), + expectedErr: "invalid character 'm' looking for beginning of value", }, } - globalFPD := make(map[string][]byte, 0) - globalFPD[siteKey] = []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`) - - openRtbGlobalFPD := make(map[string][]openrtb2.Data, 0) - openRtbGlobalFPD[siteContentDataKey] = []openrtb2.Data{ - {ID: "openRtbGlobalFPDSiteContentDataId1", Name: "openRtbGlobalFPDSiteContentDataName1"}, - {ID: "openRtbGlobalFPDSiteContentDataId2", Name: "openRtbGlobalFPDSiteContentDataName2"}, - } + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + err := mergeUser(&test.givenUser, test.givenFPD) - expectedSite := &openrtb2.Site{ - ID: "bidRequestSiteId", - Keywords: "fpdConfigSiteKeywords", - Name: "bidRequestSiteName", - Page: "bidRequestSitePage", - Content: &openrtb2.Content{ - ID: "bidRequestSiteContentId", - Episode: 4, - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDSiteContentDataId1", Name: "openRtbGlobalFPDSiteContentDataName1"}, - {ID: "openRtbGlobalFPDSiteContentDataId2", Name: "openRtbGlobalFPDSiteContentDataName2"}, - }, - }, + if test.expectedErr == "" { + assert.NoError(t, err, "unexpected error returned") + assert.Equal(t, test.expectedUser, test.givenUser, "result user is incorrect") + } else { + assert.EqualError(t, err, test.expectedErr, "expected error incorrect") + } + }) } +} +func TestMergeApp(t *testing.T) { testCases := []struct { - description string - bidRequestSiteExt []byte - expectedSiteExt string - siteContentNil bool + name string + givenApp openrtb2.App + givenFPD json.RawMessage + expectedApp openrtb2.App + expectedErr string }{ { - description: "bid request site.ext is nil", - bidRequestSiteExt: nil, - expectedSiteExt: `{"data":{ - "data":[ - {"id":"SiteDataId1","name":"SiteDataName1"}, - {"id":"SiteDataId2","name":"SiteDataName2"} - ], - "fpdConfigSiteExt":123, - "globalFPDSiteData":"globalFPDSiteDataValue", - "id":"fpdConfigSiteId" - } - }`, - siteContentNil: false, - }, - { - description: "bid request site.ext is not nil", - bidRequestSiteExt: []byte(`{"bidRequestSiteExt": 1234}`), - expectedSiteExt: `{"data":{ - "data":[ - {"id":"SiteDataId1","name":"SiteDataName1"}, - {"id":"SiteDataId2","name":"SiteDataName2"} - ], - "fpdConfigSiteExt":123, - "globalFPDSiteData":"globalFPDSiteDataValue", - "id":"fpdConfigSiteId" - }, - "bidRequestSiteExt":1234 - }`, - siteContentNil: false, - }, - { - description: "bid request site.content.data is nil ", - bidRequestSiteExt: []byte(`{"bidRequestSiteExt": 1234}`), - expectedSiteExt: `{"data":{ - "data":[ - {"id":"SiteDataId1","name":"SiteDataName1"}, - {"id":"SiteDataId2","name":"SiteDataName2"} - ], - "fpdConfigSiteExt":123, - "globalFPDSiteData":"globalFPDSiteDataValue", - "id":"fpdConfigSiteId" - }, - "bidRequestSiteExt":1234 - }`, - siteContentNil: true, + name: "empty", + givenApp: openrtb2.App{}, + givenFPD: []byte(`{}`), + expectedApp: openrtb2.App{}, + }, + { + name: "toplevel", + givenApp: openrtb2.App{ID: "1"}, + givenFPD: []byte(`{"id":"2"}`), + expectedApp: openrtb2.App{ID: "2"}, + }, + { + name: "toplevel-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`)}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, + }, + { + name: "toplevel-ext-err", + givenApp: openrtb2.App{ID: "1", Ext: []byte(`malformed`)}, + givenFPD: []byte(`{"id":"2"}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-publisher", + givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub1"}}, + givenFPD: []byte(`{"publisher":{"name": "pub2"}}`), + expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub2"}}, + }, + { + name: "nested-content", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1"}}, + givenFPD: []byte(`{"content":{"title": "content2"}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2"}}, + }, + { + name: "nested-content-producer", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, + givenFPD: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, + }, + { + name: "nested-content-network", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, + givenFPD: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, + }, + { + name: "nested-content-channel", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, + givenFPD: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, + }, + { + name: "nested-publisher-ext", + givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, + givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, + givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-producer-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenFPD: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-network-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenFPD: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-channel-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenFPD: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-publisher-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-producer-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-network-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-channel-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "nested-publisher-ext-err", + givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, + givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, + givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-producer-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, + givenFPD: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-network-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, + givenFPD: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-channel-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, + givenFPD: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), + expectedErr: "invalid request ext", + }, + { + name: "fpd-err", + givenApp: openrtb2.App{ID: "1", Ext: []byte(`{"a":1}`)}, + givenFPD: []byte(`malformed`), + expectedErr: "invalid character 'm' looking for beginning of value", }, } for _, test := range testCases { - if test.siteContentNil { - bidRequestSite.Content = nil - expectedSite.Content = &openrtb2.Content{Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDSiteContentDataId1", Name: "openRtbGlobalFPDSiteContentDataName1"}, - {ID: "openRtbGlobalFPDSiteContentDataId2", Name: "openRtbGlobalFPDSiteContentDataName2"}, - }} - } - - bidRequestSite.Ext = test.bidRequestSiteExt - - fpdConfigSite := make(map[string]json.RawMessage, 0) - fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`) - fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`) - fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`) - fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`) - fpdConfig := &openrtb_ext.ORTB2{Site: fpdConfigSite} + t.Run(test.name, func(t *testing.T) { + err := mergeApp(&test.givenApp, test.givenFPD) - resultSite, err := resolveSite(fpdConfig, bidRequestSite, globalFPD, openRtbGlobalFPD, "appnexus") - assert.NoError(t, err, "No error should be returned") - - assert.JSONEq(t, test.expectedSiteExt, string(resultSite.Ext), "Result site.Ext is incorrect") - resultSite.Ext = nil - assert.Equal(t, expectedSite, resultSite, "Result site is incorrect") + if test.expectedErr == "" { + assert.NoError(t, err, "unexpected error returned") + assert.Equal(t, test.expectedApp, test.givenApp, " result app is incorrect") + } else { + assert.EqualError(t, err, test.expectedErr, "expected error incorrect") + } + }) } - -} - -func TestResolveSiteNilValues(t *testing.T) { - resultSite, err := resolveSite(nil, nil, nil, nil, "appnexus") - assert.NoError(t, err, "No error should be returned") - assert.Nil(t, resultSite, "Result site should be nil") } -func TestResolveSiteBadInput(t *testing.T) { - fpdConfigSite := make(map[string]json.RawMessage, 0) - fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`) - fpdConfig := &openrtb_ext.ORTB2{Site: fpdConfigSite} - - resultSite, err := resolveSite(fpdConfig, nil, nil, nil, "appnexus") - assert.Error(t, err, "Error should be returned") - assert.Equal(t, "incorrect First Party Data for bidder appnexus: Site object is not defined in request, but defined in FPD config", err.Error(), "Incorrect error message") - assert.Nil(t, resultSite, "Result site should be nil") -} - -func TestMergeSites(t *testing.T) { - - originalSite := &openrtb2.Site{ - ID: "bidRequestSiteId", - Keywords: "bidRequestSiteKeywords", - Page: "bidRequestSitePage", - Name: "bidRequestSiteName", - Domain: "bidRequestSiteDomain", - Cat: []string{"books1", "magazines1"}, - SectionCat: []string{"books2", "magazines2"}, - PageCat: []string{"books3", "magazines3"}, - Search: "bidRequestSiteSearch", - Ref: "bidRequestSiteRef", - Content: &openrtb2.Content{ - Title: "bidRequestSiteContentTitle", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDSiteDataId1", Name: "openRtbGlobalFPDSiteDataName1"}, - {ID: "openRtbGlobalFPDSiteDataId2", Name: "openRtbGlobalFPDSiteDataName2"}, - }, +func TestMergeSite(t *testing.T) { + testCases := []struct { + name string + givenSite openrtb2.Site + givenFPD json.RawMessage + expectedSite openrtb2.Site + expectedErr string + }{ + { + name: "empty", + givenSite: openrtb2.Site{}, + givenFPD: []byte(`{}`), + expectedErr: "incorrect First Party Data for bidder BidderA: Site object cannot set empty page if req.site.id is empty", }, - Ext: []byte(`{"bidRequestSiteExt": 1234}`), - } - fpdConfigSite := make(map[string]json.RawMessage, 0) - fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`) - fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`) - fpdConfigSite[pageKey] = []byte(`"fpdConfigSitePage"`) - fpdConfigSite[nameKey] = []byte(`"fpdConfigSiteName"`) - fpdConfigSite[domainKey] = []byte(`"fpdConfigSiteDomain"`) - fpdConfigSite[catKey] = []byte(`["cars1", "auto1"]`) - fpdConfigSite[sectionCatKey] = []byte(`["cars2", "auto2"]`) - fpdConfigSite[pageCatKey] = []byte(`["cars3", "auto3"]`) - fpdConfigSite[searchKey] = []byte(`"fpdConfigSiteSearch"`) - fpdConfigSite[refKey] = []byte(`"fpdConfigSiteRef"`) - fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`) - fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`) - - resultSite, err := mergeSites(originalSite, fpdConfigSite, "appnexus") - assert.NoError(t, err, "No error should be returned") - - expectedSiteExt := `{"bidRequestSiteExt":1234, - "data":{ - "data":[ - {"id":"SiteDataId1","name":"SiteDataName1"}, - {"id":"SiteDataId2","name":"SiteDataName2"}], - "fpdConfigSiteExt":123, - "id":"fpdConfigSiteId"} - }` - assert.JSONEq(t, expectedSiteExt, string(resultSite.Ext), "Result user.Ext is incorrect") - resultSite.Ext = nil - - expectedSite := openrtb2.Site{ - ID: "bidRequestSiteId", - Keywords: "fpdConfigSiteKeywords", - Page: "fpdConfigSitePage", - Name: "fpdConfigSiteName", - Domain: "fpdConfigSiteDomain", - Cat: []string{"cars1", "auto1"}, - SectionCat: []string{"cars2", "auto2"}, - PageCat: []string{"cars3", "auto3"}, - Search: "fpdConfigSiteSearch", - Ref: "fpdConfigSiteRef", - Content: &openrtb2.Content{ - Title: "bidRequestSiteContentTitle", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDSiteDataId1", Name: "openRtbGlobalFPDSiteDataName1"}, - {ID: "openRtbGlobalFPDSiteDataId2", Name: "openRtbGlobalFPDSiteDataName2"}, - }, + { + name: "toplevel", + givenSite: openrtb2.Site{ID: "1"}, + givenFPD: []byte(`{"id":"2"}`), + expectedSite: openrtb2.Site{ID: "2"}, }, - Ext: nil, - } - assert.Equal(t, expectedSite, resultSite, "Result user is incorrect") -} - -func TestResolveApp(t *testing.T) { - - fpdConfigApp := make(map[string]json.RawMessage, 0) - fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`) - fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`) - fpdConfigApp[nameKey] = []byte(`"fpdConfigAppName"`) - fpdConfigApp[bundleKey] = []byte(`"fpdConfigAppBundle"`) - fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`) - fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`) - - bidRequestApp := &openrtb2.App{ - ID: "bidRequestAppId", - Keywords: "bidRequestAppKeywords", - Name: "bidRequestAppName", - Bundle: "bidRequestAppBundle", - Content: &openrtb2.Content{ - ID: "bidRequestAppContentId", - Episode: 4, - Data: []openrtb2.Data{ - {ID: "bidRequestAppContentDataId1", Name: "bidRequestAppContentDataName1"}, - {ID: "bidRequestAppContentDataId2", Name: "bidRequestAppContentDataName2"}, - }, + { + name: "toplevel-ext", + givenSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":2}`)}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), + expectedSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":100,"c":3}`)}, + }, + { + name: "toplevel-ext-err", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`malformed`)}, + givenFPD: []byte(`{"id":"2"}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-publisher", + givenSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub1"}}, + givenFPD: []byte(`{"publisher":{"name": "pub2"}}`), + expectedSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub2"}}, + }, + { + name: "nested-content", + givenSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content1"}}, + givenFPD: []byte(`{"content":{"title": "content2"}}`), + expectedSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content2"}}, + }, + { + name: "nested-content-producer", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, + givenFPD: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, + }, + { + name: "nested-content-network", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, + givenFPD: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, + }, + { + name: "nested-content-channel", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, + givenFPD: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, + }, + { + name: "nested-publisher-ext", + givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, + givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, + givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-producer-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenFPD: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-network-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenFPD: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-channel-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenFPD: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-publisher-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-producer-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-network-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-channel-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "nested-publisher-ext-err", + givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, + givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, + givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-producer-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, + givenFPD: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-network-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, + givenFPD: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectedErr: "invalid request ext", + }, + { + name: "nested-content-channel-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, + givenFPD: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), + expectedErr: "invalid request ext", + }, + { + name: "fpd-err", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1}`)}, + givenFPD: []byte(`malformed`), + expectedErr: "invalid character 'm' looking for beginning of value", }, } - globalFPD := make(map[string][]byte, 0) - globalFPD[appKey] = []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`) - - openRtbGlobalFPD := make(map[string][]openrtb2.Data, 0) - openRtbGlobalFPD[appContentDataKey] = []openrtb2.Data{ - {ID: "openRtbGlobalFPDAppContentDataId1", Name: "openRtbGlobalFPDAppContentDataName1"}, - {ID: "openRtbGlobalFPDAppContentDataId2", Name: "openRtbGlobalFPDAppContentDataName2"}, - } + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + err := mergeSite(&test.givenSite, test.givenFPD, "BidderA") - expectedApp := &openrtb2.App{ - ID: "bidRequestAppId", - Keywords: "fpdConfigAppKeywords", - Name: "bidRequestAppName", - Bundle: "bidRequestAppBundle", - Content: &openrtb2.Content{ - ID: "bidRequestAppContentId", - Episode: 4, - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDAppContentDataId1", Name: "openRtbGlobalFPDAppContentDataName1"}, - {ID: "openRtbGlobalFPDAppContentDataId2", Name: "openRtbGlobalFPDAppContentDataName2"}, - }, - }, + if test.expectedErr == "" { + assert.NoError(t, err, "unexpected error returned") + assert.Equal(t, test.expectedSite, test.givenSite, " result Site is incorrect") + } else { + assert.EqualError(t, err, test.expectedErr, "expected error incorrect") + } + }) } +} +// TestMergeObjectStructure detects when new nested objects are added to First Party Data supported +// fields, as these will invalidate the mergeSite, mergeApp, and mergeUser methods. If this test fails, +// fix the merge methods to add support and update this test to set a new baseline. +func TestMergeObjectStructure(t *testing.T) { testCases := []struct { - description string - bidRequestAppExt []byte - expectedAppExt string - appContentNil bool + name string + kind any + knownStructs []string }{ { - description: "bid request app.ext is nil", - bidRequestAppExt: nil, - expectedAppExt: `{"data":{ - "data":[ - {"id":"AppDataId1","name":"AppDataName1"}, - {"id":"AppDataId2","name":"AppDataName2"} - ], - "fpdConfigAppExt":123, - "globalFPDAppData":"globalFPDAppDataValue", - "id":"fpdConfigAppId" - } - }`, - appContentNil: false, - }, - { - description: "bid request app.ext is not nil", - bidRequestAppExt: []byte(`{"bidRequestAppExt": 1234}`), - expectedAppExt: `{"data":{ - "data":[ - {"id":"AppDataId1","name":"AppDataName1"}, - {"id":"AppDataId2","name":"AppDataName2"} - ], - "fpdConfigAppExt":123, - "globalFPDAppData":"globalFPDAppDataValue", - "id":"fpdConfigAppId" - }, - "bidRequestAppExt":1234 - }`, - appContentNil: false, - }, - { - description: "bid request app.content.data is nil ", - bidRequestAppExt: []byte(`{"bidRequestAppExt": 1234}`), - expectedAppExt: `{"data":{ - "data":[ - {"id":"AppDataId1","name":"AppDataName1"}, - {"id":"AppDataId2","name":"AppDataName2"} - ], - "fpdConfigAppExt":123, - "globalFPDAppData":"globalFPDAppDataValue", - "id":"fpdConfigAppId" - }, - "bidRequestAppExt":1234 - }`, - appContentNil: true, + name: "Site", + kind: openrtb2.Site{}, + knownStructs: []string{ + "Publisher", + "Content", + "Content.Producer", + "Content.Network", + "Content.Channel", + }, + }, + { + name: "App", + kind: openrtb2.App{}, + knownStructs: []string{ + "Publisher", + "Content", + "Content.Producer", + "Content.Network", + "Content.Channel", + }, + }, + { + name: "User", + kind: openrtb2.User{}, + knownStructs: []string{ + "Geo", + }, }, } for _, test := range testCases { - if test.appContentNil { - bidRequestApp.Content = nil - expectedApp.Content = &openrtb2.Content{Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDAppContentDataId1", Name: "openRtbGlobalFPDAppContentDataName1"}, - {ID: "openRtbGlobalFPDAppContentDataId2", Name: "openRtbGlobalFPDAppContentDataName2"}, - }} - } - - bidRequestApp.Ext = test.bidRequestAppExt - - fpdConfigApp := make(map[string]json.RawMessage, 0) - fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`) - fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`) - fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`) - fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`) - fpdConfig := &openrtb_ext.ORTB2{App: fpdConfigApp} - - resultApp, err := resolveApp(fpdConfig, bidRequestApp, globalFPD, openRtbGlobalFPD, "appnexus") - assert.NoError(t, err, "No error should be returned") + t.Run(test.name, func(t *testing.T) { + nestedStructs := []string{} + + var discover func(parent string, t reflect.Type) + discover = func(parent string, t reflect.Type) { + fields := reflect.VisibleFields(t) + for _, field := range fields { + if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { + nestedStructs = append(nestedStructs, parent+field.Name) + discover(parent+field.Name+".", field.Type.Elem()) + } + } + } + discover("", reflect.TypeOf(test.kind)) - assert.JSONEq(t, test.expectedAppExt, string(resultApp.Ext), "Result app.Ext is incorrect") - resultApp.Ext = nil - assert.Equal(t, expectedApp, resultApp, "Result app is incorrect") + assert.ElementsMatch(t, test.knownStructs, nestedStructs) + }) } - } -func TestResolveAppNilValues(t *testing.T) { - resultApp, err := resolveApp(nil, nil, nil, nil, "appnexus") - assert.NoError(t, err, "No error should be returned") - assert.Nil(t, resultApp, "Result app should be nil") -} +// user memory protect test +func TestMergeUserMemoryProtection(t *testing.T) { + inputGeo := &openrtb2.Geo{ + Ext: json.RawMessage(`{"a":1,"b":2}`), + } + input := openrtb2.User{ + ID: "1", + Geo: inputGeo, + } -func TestResolveAppBadInput(t *testing.T) { - fpdConfigApp := make(map[string]json.RawMessage, 0) - fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`) - fpdConfig := &openrtb_ext.ORTB2{App: fpdConfigApp} + err := mergeUser(&input, userFPD) + assert.NoError(t, err) - resultApp, err := resolveApp(fpdConfig, nil, nil, nil, "appnexus") - assert.Error(t, err, "Error should be returned") - assert.Equal(t, "incorrect First Party Data for bidder appnexus: App object is not defined in request, but defined in FPD config", err.Error(), "Incorrect error message") - assert.Nil(t, resultApp, "Result app should be nil") + // Input user object is expected to be a copy. Changes are ok. + assert.Equal(t, "2", input.ID, "user-id-copied") + + // Nested objects must be copied before changes. + assert.JSONEq(t, `{"a":1,"b":2}`, string(inputGeo.Ext), "geo-input") + assert.JSONEq(t, `{"a":1,"b":100,"c":3}`, string(input.Geo.Ext), "geo-copied") } -func TestMergeApps(t *testing.T) { - - originalApp := &openrtb2.App{ - ID: "bidRequestAppId", - Keywords: "bidRequestAppKeywords", - Name: "bidRequestAppName", - Domain: "bidRequestAppDomain", - Bundle: "bidRequestAppBundle", - StoreURL: "bidRequestAppStoreUrl", - Ver: "bidRequestAppVer", - Cat: []string{"books1", "magazines1"}, - SectionCat: []string{"books2", "magazines2"}, - PageCat: []string{"books3", "magazines3"}, - Content: &openrtb2.Content{ - Title: "bidRequestAppContentTitle", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDAppDataId1", Name: "openRtbGlobalFPDAppDataName1"}, - {ID: "openRtbGlobalFPDAppDataId2", Name: "openRtbGlobalFPDAppDataName2"}, - }, - }, - Ext: []byte(`{"bidRequestAppExt": 1234}`), +// app memory protect test +func TestMergeAppMemoryProtection(t *testing.T) { + inputPublisher := &openrtb2.Publisher{ + ID: "InPubId", + Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`), } - fpdConfigApp := make(map[string]json.RawMessage, 0) - fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`) - fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`) - fpdConfigApp[nameKey] = []byte(`"fpdConfigAppName"`) - fpdConfigApp[domainKey] = []byte(`"fpdConfigAppDomain"`) - fpdConfigApp[bundleKey] = []byte(`"fpdConfigAppBundle"`) - fpdConfigApp[storeUrlKey] = []byte(`"fpdConfigAppStoreUrl"`) - fpdConfigApp[verKey] = []byte(`"fpdConfigAppVer"`) - fpdConfigApp[catKey] = []byte(`["cars1", "auto1"]`) - fpdConfigApp[sectionCatKey] = []byte(`["cars2", "auto2"]`) - fpdConfigApp[pageCatKey] = []byte(`["cars3", "auto3"]`) - fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`) - fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`) - - resultApp, err := mergeApps(originalApp, fpdConfigApp) - assert.NoError(t, err, "No error should be returned") - - expectedAppExt := `{"bidRequestAppExt":1234, - "data":{ - "data":[ - {"id":"AppDataId1","name":"AppDataName1"}, - {"id":"AppDataId2","name":"AppDataName2"}], - "fpdConfigAppExt":123, - "id":"fpdConfigAppId"} - }` - assert.JSONEq(t, expectedAppExt, string(resultApp.Ext), "Result user.Ext is incorrect") - resultApp.Ext = nil - - expectedApp := openrtb2.App{ - ID: "bidRequestAppId", - Keywords: "fpdConfigAppKeywords", - Name: "fpdConfigAppName", - Domain: "fpdConfigAppDomain", - Bundle: "fpdConfigAppBundle", - Ver: "fpdConfigAppVer", - StoreURL: "fpdConfigAppStoreUrl", - Cat: []string{"cars1", "auto1"}, - SectionCat: []string{"cars2", "auto2"}, - PageCat: []string{"cars3", "auto3"}, - Content: &openrtb2.Content{ - Title: "bidRequestAppContentTitle", - Data: []openrtb2.Data{ - {ID: "openRtbGlobalFPDAppDataId1", Name: "openRtbGlobalFPDAppDataName1"}, - {ID: "openRtbGlobalFPDAppDataId2", Name: "openRtbGlobalFPDAppDataName2"}, - }, + inputContent := &openrtb2.Content{ + ID: "InContentId", + Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`), + Producer: &openrtb2.Producer{ + ID: "InProducerId", + Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`), }, - Ext: nil, + Network: &openrtb2.Network{ + ID: "InNetworkId", + Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`), + }, + Channel: &openrtb2.Channel{ + ID: "InChannelId", + Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`), + }, + } + input := openrtb2.App{ + ID: "InAppID", + Publisher: inputPublisher, + Content: inputContent, + Ext: json.RawMessage(`{"a": "inputAppExt", "b": 1}`), } - assert.Equal(t, expectedApp, resultApp, "Result user is incorrect") + + err := mergeApp(&input, fpdWithPublisherAndContent) + assert.NoError(t, err) + + // Input app object is expected to be a copy. Changes are ok. + assert.Equal(t, "FPDID", input.ID, "app-id-copied") + assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "app-ext-copied") + + // Nested objects must be copied before changes. + assert.Equal(t, "InPubId", inputPublisher.ID, "app-pub-id-input") + assert.Equal(t, "FPDPubId", input.Publisher.ID, "app-pub-id-copied") + assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "app-pub-ext-input") + assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "app-pub-ext-copied") + + assert.Equal(t, "InContentId", inputContent.ID, "app-content-id-input") + assert.Equal(t, "FPDContentId", input.Content.ID, "app-content-id-copied") + assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "app-content-ext-input") + assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "app-content-ext-copied") + + assert.Equal(t, "InProducerId", inputContent.Producer.ID, "app-content-producer-id-input") + assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "app-content-producer-id-copied") + assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "app-content-producer-ext-input") + assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "app-content-producer-ext-copied") + + assert.Equal(t, "InNetworkId", inputContent.Network.ID, "app-content-network-id-input") + assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "app-content-network-id-copied") + assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "app-content-network-ext-input") + assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "app-content-network-ext-copied") + + assert.Equal(t, "InChannelId", inputContent.Channel.ID, "app-content-channel-id-input") + assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "app-content-channel-id-copied") + assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "app-content-channel-ext-input") + assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "app-content-channel-ext-copied") } -func TestBuildExtData(t *testing.T) { - testCases := []struct { - description string - input []byte - expectedRes string - }{ - { - description: "Input object with int value", - input: []byte(`{"someData": 123}`), - expectedRes: `{"data": {"someData": 123}}`, - }, - { - description: "Input object with bool value", - input: []byte(`{"someData": true}`), - expectedRes: `{"data": {"someData": true}}`, +// site memory protect test +func TestMergeSiteMemoryProtection(t *testing.T) { + inputPublisher := &openrtb2.Publisher{ + ID: "InPubId", + Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`), + } + inputContent := &openrtb2.Content{ + ID: "InContentId", + Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`), + Producer: &openrtb2.Producer{ + ID: "InProducerId", + Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`), }, - { - description: "Input object with string value", - input: []byte(`{"someData": "true"}`), - expectedRes: `{"data": {"someData": "true"}}`, + Network: &openrtb2.Network{ + ID: "InNetworkId", + Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`), }, - { - description: "No input object", - input: []byte(`{}`), - expectedRes: `{"data": {}}`, - }, - { - description: "Input object with object value", - input: []byte(`{"someData": {"moreFpdData": "fpddata"}}`), - expectedRes: `{"data": {"someData": {"moreFpdData": "fpddata"}}}`, + Channel: &openrtb2.Channel{ + ID: "InChannelId", + Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`), }, } - - for _, test := range testCases { - actualRes := buildExtData(test.input) - assert.JSONEq(t, test.expectedRes, string(actualRes), "Incorrect result data") + input := openrtb2.Site{ + ID: "InSiteID", + Publisher: inputPublisher, + Content: inputContent, + Ext: json.RawMessage(`{"a": "inputSiteExt", "b": 1}`), } + + err := mergeSite(&input, fpdWithPublisherAndContent, "BidderA") + assert.NoError(t, err) + + // Input app object is expected to be a copy. Changes are ok. + assert.Equal(t, "FPDID", input.ID, "site-id-copied") + assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "site-ext-copied") + + // Nested objects must be copied before changes. + assert.Equal(t, "InPubId", inputPublisher.ID, "site-pub-id-input") + assert.Equal(t, "FPDPubId", input.Publisher.ID, "site-pub-id-copied") + assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "site-pub-ext-input") + assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "site-pub-ext-copied") + + assert.Equal(t, "InContentId", inputContent.ID, "site-content-id-input") + assert.Equal(t, "FPDContentId", input.Content.ID, "site-content-id-copied") + assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "site-content-ext-input") + assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "site-content-ext-copied") + + assert.Equal(t, "InProducerId", inputContent.Producer.ID, "site-content-producer-id-input") + assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "site-content-producer-id-copied") + assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "site-content-producer-ext-input") + assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "site-content-producer-ext-copied") + + assert.Equal(t, "InNetworkId", inputContent.Network.ID, "site-content-network-id-input") + assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "site-content-network-id-copied") + assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "site-content-network-ext-input") + assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "site-content-network-ext-copied") + + assert.Equal(t, "InChannelId", inputContent.Channel.ID, "site-content-channel-id-input") + assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "site-content-channel-id-copied") + assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "site-content-channel-ext-input") + assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "site-content-channel-ext-copied") +} + +var ( + userFPD = []byte(` +{ + "id": "2", + "geo": { + "ext": { + "b": 100, + "c": 3 + } + } } +`) + + fpdWithPublisherAndContent = []byte(` +{ + "id": "FPDID", + "ext": {"a": "FPDExt", "b": 2}, + "publisher": { + "id": "FPDPubId", + "ext": {"a": "FPDPubExt", "b": 2} + }, + "content": { + "id": "FPDContentId", + "ext": {"a": "FPDContentExt", "b": 2}, + "producer": { + "id": "FPDProducerId", + "ext": {"a": "FPDProducerExt", "b": 2} + }, + "network": { + "id": "FPDNetworkId", + "ext": {"a": "FPDNetworkExt", "b": 2} + }, + "channel": { + "id": "FPDChannelId", + "ext": {"a": "FPDChannelExt", "b": 2} + } + } +} +`) + + user = []byte(` +{ + "id": "2", + "yob": 2000, + "geo": { + "city": "LA", + "ext": { + "b": 100, + "c": 3 + } + } +} +`) +) diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json index 6d2d54f6508..751a0bec292 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json @@ -11,10 +11,8 @@ } ] }, - "outputRequestData": { - }, + "outputRequestData": {}, "bidderConfigFPD": { "appnexus": {} } -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json index 3bafae6dc87..d7a6e76485a 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json @@ -13,10 +13,8 @@ } ] }, - "outputRequestData": { - }, + "outputRequestData": {}, "bidderConfigFPD": { - "appnexus": { - } + "appnexus": {} } -} +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json index 356b84aac35..bf9d1843c84 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json @@ -56,8 +56,8 @@ "outputRequestData": {}, "bidderConfigFPD": {}, "validationErrors": [ - {"Message": "multiple First Party Data bidder configs provided for bidder: appnexus"} + { + "Message": "multiple First Party Data bidder configs provided for bidder: appnexus" + } ] -} - - +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json index d127e5df15f..5caba3359c3 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json @@ -6,6 +6,4 @@ }, "outputRequestData": {}, "bidderConfigFPD": {} -} - - +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json index 48ea27a7c42..c54daf0c7b7 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json @@ -52,8 +52,7 @@ } ] }, - "outputRequestData": { - }, + "outputRequestData": {}, "bidderConfigFPD": { "appnexus": { "site": { @@ -106,4 +105,4 @@ } } } -} +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json index d3dfc5b0943..e6e2c4596d5 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json @@ -3,5 +3,4 @@ "inputRequestData": {}, "outputRequestData": {}, "bidderConfigFPD": {} -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json index 1063f7ecf40..697bc8972f8 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json @@ -6,6 +6,4 @@ }, "outputRequestData": {}, "bidderConfigFPD": {} -} - - +} \ No newline at end of file diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json index 6c1938bc7c0..ae5bc578a19 100644 --- a/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json +++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json @@ -11,5 +11,4 @@ }, "outputRequestData": {}, "bidderConfigFPD": {} -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json index 4f312b3dc01..c8649d40a40 100644 --- a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json +++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json @@ -55,7 +55,7 @@ "biddersFPDResolved": { "appnexus": { "app": { - "id": "reqAppId", + "id": "apnAppId", "name": "fpdAppName", "bundle": "fpdAppBundle", "domain": "fpdAppDomain", @@ -67,11 +67,6 @@ "ver": "1.2", "publisher": { "id": "1" - }, - "ext": { - "data": { - "id": "apnAppId" - } } } } diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json index 5fc7c0d7cda..7916c3f3e8d 100644 --- a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json +++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json @@ -53,7 +53,7 @@ "biddersFPDResolved": { "appnexus": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "keywords": "apnKeywords", "sectioncat": ["books"], "pagecat": ["magazines"], @@ -61,11 +61,6 @@ "page": "http://www.foobar.com/testurl.html", "publisher": { "id": "1" - }, - "ext": { - "data": { - "id": "apnSiteId" - } } } } diff --git a/firstpartydata/tests/extractfpdforbidders/req-empty-user-fpd-not-empty-user.json b/firstpartydata/tests/extractfpdforbidders/req-empty-user-fpd-not-empty-user.json index 7ba65d1b075..1469af16eb6 100644 --- a/firstpartydata/tests/extractfpdforbidders/req-empty-user-fpd-not-empty-user.json +++ b/firstpartydata/tests/extractfpdforbidders/req-empty-user-fpd-not-empty-user.json @@ -50,6 +50,7 @@ "biddersFPDResolved": { "appnexus": { "user": { + "id": "appnexusFpdUser", "yob": 2011, "gender": "F", "keywords": "fpd keywords", @@ -65,12 +66,7 @@ ], "ext": { "data": { - "ext": { - "data": { - "userdata": "appnexusFpdUserExtData" - } - }, - "id": "appnexusFpdUser" + "userdata": "appnexusFpdUserExtData" } } } diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json index 62b2a44eead..26bb054f19d 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json @@ -67,24 +67,14 @@ "biddersFPDResolved": { "appnexus": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "publisher": { "id": "1" - }, - "ext": { - "data": { - "id": "apnSiteId" - } } }, "user": { - "id": "reqUserId", - "ext": { - "data": { - "id": "apnUserId" - } - } + "id": "apnUserId" } }, "telaria": { diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-content-data.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-content-data.json index 2ae0456a4cb..76254fb096a 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-content-data.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-content-data.json @@ -121,7 +121,7 @@ "biddersFPDResolved": { "appnexus": { "app": { - "id": "reqAppId", + "id": "apnFpdAppId", "name": "apnFpdAppIdAppName", "cat": [ "books", @@ -144,24 +144,11 @@ "name": "apnFpdAppIdContentDataName2" } ] - }, - "ext": { - "data": { - "id": "apnFpdAppId", - "publisher": { - "id": "1" - } - } } }, "user": { - "id": "reqUserId", - "keywords": "apnFpdUserKeyword", - "ext": { - "data": { - "id":"apnUserId" - } - } + "id": "apnUserId", + "keywords": "apnFpdUserKeyword" } } }, diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-content-data.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-content-data.json index 5dfcee9cd52..e7c7514d402 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-content-data.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-content-data.json @@ -122,7 +122,7 @@ "biddersFPDResolved": { "appnexus": { "site": { - "id": "reqSiteId", + "id": "apnFpdSiteId", "page": "http://www.foobar.com/4321.html", "name": "apnFpdSiteIdSiteName", "cat": [ @@ -146,24 +146,11 @@ "name": "apnFpdSiteIdContentDataName2" } ] - }, - "ext": { - "data": { - "id": "apnFpdSiteId", - "publisher": { - "id": "1" - } - } } }, "user": { - "id": "reqUserId", - "keywords": "apnFpdUserKeyword", - "ext": { - "data": { - "id":"apnUserId" - } - } + "id": "apnUserId", + "keywords": "apnFpdUserKeyword" } } }, diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json index 88a79002f26..e5ea399402f 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json @@ -146,7 +146,7 @@ "biddersFPDResolved": { "appnexus": { "site": { - "id": "reqSiteId", + "id": "apnFpdSiteId", "page": "http://www.foobar.com/4321.html", "name": "apnFpdSiteIdSiteName", "cat": [ @@ -170,18 +170,10 @@ "name": "apnFpdSiteIdContentDataName2" } ] - }, - "ext": { - "data": { - "id": "apnFpdSiteId", - "publisher": { - "id": "1" - } - } } }, "user": { - "id": "reqUserId", + "id": "apnUserId", "gender": "F", "yob": 2000, "keywords": "apnFpdUserKeyword", @@ -194,12 +186,7 @@ "id": "apnFpdUserContentDataId2", "name": "apnFpdUserContentDataName2" } - ], - "ext": { - "data": { - "id": "apnUserId" - } - } + ] } } }, diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-app-content-data.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-app-content-data.json index cbebe0fc89b..731e08171ae 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-app-content-data.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-app-content-data.json @@ -107,7 +107,7 @@ "biddersFPDResolved": { "appnexus": { "app": { - "id": "reqAppId", + "id": "apnFpdAppId", "name": "apnFpdAppIdAppName", "cat": [ "books", @@ -130,24 +130,11 @@ "name": "apnFpdAppIdContentDataName2" } ] - }, - "ext": { - "data": { - "id": "apnFpdAppId", - "publisher": { - "id": "1" - } - } } }, "user": { - "id": "reqUserId", - "keywords": "apnFpdUserKeyword", - "ext": { - "data": { - "id":"apnUserId" - } - } + "id": "apnUserId", + "keywords": "apnFpdUserKeyword" } }, "telaria": { diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-site-content-data.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-site-content-data.json index 717d174102a..efb250b7040 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-site-content-data.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-without-config-and-global-site-content-data.json @@ -110,7 +110,7 @@ "biddersFPDResolved": { "appnexus": { "site": { - "id": "reqSiteId", + "id": "apnFpdAppId", "name": "apnFpdAppIdAppName", "page": "http://www.foobar.com/1234.html", "cat": [ @@ -134,24 +134,11 @@ "name": "apnFpdAppIdContentDataName2" } ] - }, - "ext": { - "data": { - "id": "apnFpdAppId", - "publisher": { - "id": "1" - } - } } }, "user": { - "id": "reqUserId", - "keywords": "apnFpdUserKeyword", - "ext": { - "data": { - "id":"apnUserId" - } - } + "id": "apnUserId", + "keywords": "apnFpdUserKeyword" } }, "telaria": { diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json index d86db6dc4e4..70798dfdfdf 100644 --- a/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json +++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json @@ -67,24 +67,17 @@ "biddersFPDResolved": { "appnexus": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "publisher": { "id": "1" }, "ext": { - "data":{ - "id":"apnSiteId" - } + "data": "fpdGlobalSiteData" } }, "user": { - "id": "reqUserId", - "ext": { - "data":{ - "id":"apnUserId" - } - } + "id": "apnUserId" } }, "telaria": { diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json b/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json index db579fb2a41..812db7b10b5 100644 --- a/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json +++ b/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json @@ -1,44 +1,30 @@ { - "description": "Bidder FPD defined only for app", - "inputRequestData": { - "app": { - "id": "reqUserID" - } - }, - "bidderConfigFPD": { - "appnexus": { - "app": { - "id": "apnAppId", - "ext": { - "data": { - "morefpdData": "morefpddata", - "appFpddata": "appFpddata", - "moreFpd": { - "fpd": 123 + "description": "Bidder FPD defined only for app", + "inputRequestData": { + "app": { + "id": "reqUserID" + } + }, + "bidderConfigFPD": { + "appnexus": { + "app": { + "id": "apnAppId", + "ext": { + "data": { + "other": "data" + } + } } - } } - } - } - }, - "outputRequestData": { - "app": { - "id": "reqUserID", - "ext": { - "data": { - "ext": { - "data": { - "morefpdData": "morefpddata", - "appFpddata": "appFpddata", - "moreFpd": { - "fpd": 123 - } + }, + "outputRequestData": { + "app": { + "id": "apnAppId", + "ext": { + "data": { + "other": "data" + } } - }, - "id": "apnAppId" } - } } - } -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json b/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json index 86a40f29d75..5accfc3b3a0 100644 --- a/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json +++ b/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json @@ -1,44 +1,30 @@ { - "description": "Bidder FPD defined only for site", - "inputRequestData": { - "site": { - "id": "reqUserID" - } - }, - "bidderConfigFPD": { - "appnexus": { - "site": { - "id": "apnSiteId", - "ext": { - "data": { - "morefpdData": "morefpddata", - "siteFpddata": "siteFpddata", - "moreFpd": { - "fpd": 123 + "description": "Bidder FPD defined only for site", + "inputRequestData": { + "site": { + "id": "reqUserID" + } + }, + "bidderConfigFPD": { + "appnexus": { + "site": { + "id": "apnSiteId", + "ext": { + "data": { + "other": "data" + } + } } - } } - } - } - }, - "outputRequestData": { - "site": { - "id": "reqUserID", - "ext": { - "data": { - "ext": { - "data": { - "morefpdData": "morefpddata", - "siteFpddata": "siteFpddata", - "moreFpd": { - "fpd": 123 - } + }, + "outputRequestData": { + "site": { + "id": "apnSiteId", + "ext": { + "data": { + "other": "data" + } } - }, - "id": "apnSiteId" } - } } - } -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json b/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json index a1fb4b4190f..606cee7dbe6 100644 --- a/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json +++ b/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json @@ -1,49 +1,35 @@ { - "description": "Bidder FPD defined only for user", - "inputRequestData": { - "user": { - "id": "reqUserID", - "yob": 1982, - "gender": "M" - } - }, - "bidderConfigFPD": { - "appnexus": { - "user": { - "id": "apnUserId", - "yob": 1982, - "ext": { - "data": { - "morefpdData": "morefpddata", - "userFpddata": "userFpddata", - "moreFpd": { - "fpd": 123 + "description": "Bidder FPD defined only for user", + "inputRequestData": { + "user": { + "id": "reqUserID", + "yob": 1982, + "gender": "M" + } + }, + "bidderConfigFPD": { + "appnexus": { + "user": { + "id": "apnUserId", + "yob": 1982, + "ext": { + "data": { + "other": "data" + } + } } - } } - } - } - }, - "outputRequestData": { - "user": { - "id": "reqUserID", - "yob": 1982, - "gender": "M", - "ext": { - "data": { - "ext": { - "data": { - "morefpdData": "morefpddata", - "userFpddata": "userFpddata", - "moreFpd": { - "fpd": 123 - } + }, + "outputRequestData": { + "user": { + "id": "apnUserId", + "yob": 1982, + "gender": "M", + "ext": { + "data": { + "other": "data" + } } - }, - "id": "apnUserId" } - } } - } -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-site-content-data-only.json b/firstpartydata/tests/resolvefpd/bidder-fpd-site-content-data-only.json index 723d934c6e9..541d51f42af 100644 --- a/firstpartydata/tests/resolvefpd/bidder-fpd-site-content-data-only.json +++ b/firstpartydata/tests/resolvefpd/bidder-fpd-site-content-data-only.json @@ -48,11 +48,7 @@ }, "ext": { "data": { - "morefpdData": "morefpddata", - "siteFpddata": "siteFpddata", - "moreFpd": { - "fpd": 123 - } + "other": "data" } } } @@ -65,7 +61,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "name": "apnSiteName", "domain": "apnSiteDomain", "page": "http://www.foobar.com/1234.html", @@ -92,13 +88,8 @@ }, "ext": { "data": { - "id": "apnSiteId", - "morefpdData": "morefpddata", - "siteFpddata": "siteFpddata", - "testSiteFpd":"testSite", - "moreFpd": { - "fpd": 123 - } + "other": "data", + "testSiteFpd": "testSite" } } }, @@ -109,5 +100,4 @@ "ifa": "123" } } -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json index ebba9041788..16b5332e942 100644 --- a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json +++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json @@ -11,7 +11,7 @@ "user": { "id": "reqUserID", "yob": 1982, - "gender": "M" + "gender": "reqUserGender" } }, "bidderConfigFPD": { @@ -20,11 +20,7 @@ "id": "apnAppId", "ext": { "data": { - "morefpdData": "morefpddata", - "appFpddata": "appFpddata", - "moreFpd": { - "fpd": 123 - } + "morefpdData": "morefpddata" } } } @@ -60,7 +56,7 @@ }, "outputRequestData": { "app": { - "id": "reqAppId", + "id": "apnAppId", "page": "http://www.foobar.com/1234.html", "publisher": { "id": "1" @@ -79,20 +75,15 @@ }, "ext": { "data": { - "moreFpd": { - "fpd": 123 - }, - "id": "apnAppId", "morefpdData": "morefpddata", - "appFpd": 123, - "appFpddata": "appFpddata" + "appFpd": 123 } } }, "user": { "id": "reqUserID", "yob": 1982, - "gender": "M", + "gender": "reqUserGender", "data": [ { "id": "userData1", @@ -110,5 +101,4 @@ } } } -} - +} \ No newline at end of file diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json index dea874147ab..609ede597cf 100644 --- a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json +++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json @@ -45,7 +45,7 @@ }, "outputRequestData": { "app": { - "id": "reqAppId", + "id": "apnAppId", "page": "http://www.foobar.com/1234.html", "publisher": { "id": "1" @@ -59,7 +59,6 @@ }, "ext": { "data": { - "id": "apnAppId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json index fc65518dcd3..f66a8722308 100644 --- a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json +++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json @@ -52,7 +52,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "publisher": { "id": "1" @@ -66,16 +66,11 @@ }, "ext": { "data": { - "ext": { - "data": { - "morefpdData": "morefpddata", - "siteFpddata": "siteFpddata", - "moreFpd": { - "fpd": 123 - } - } - }, - "id": "apnSiteId" + "morefpdData": "morefpddata", + "siteFpddata": "siteFpddata", + "moreFpd": { + "fpd": 123 + } } } }, @@ -83,7 +78,6 @@ "id": "apnUserId", "ext": { "data": { - "id": "apnUserId", "moreFpd": { "fpd": 567 }, diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json index f3d3fb74df2..be03f0cb9b0 100644 --- a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json +++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json @@ -41,7 +41,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "ref": "fpdRef", "publisher": { @@ -56,7 +56,6 @@ }, "ext": { "data": { - "id": "apnSiteId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json index 612518655d9..ef381d613e6 100644 --- a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json +++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json @@ -31,12 +31,11 @@ }, "outputRequestData": { "user": { - "id": "reqUserID", + "id": "apnUserId", "yob": 1982, "gender": "M", "ext": { "data": { - "id": "apnUserId", "testUserFpd": "testuser", "morefpdData": "morefpddata", "userFpddata": "siteFpddata", diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json b/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json index d0154d1bae6..c0f4723d62a 100644 --- a/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json +++ b/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json @@ -63,7 +63,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "ref": "fpdRef", "publisher": { @@ -83,7 +83,6 @@ }, "ext": { "data": { - "id": "apnSiteId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-app-user.json b/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-app-user.json index 17acd451ee8..a705e8b2405 100644 --- a/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-app-user.json +++ b/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-app-user.json @@ -73,7 +73,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "ref": "fpdRef", "publisher": { @@ -99,7 +99,6 @@ "ext": { "testSiteExt": 123, "data": { - "id": "apnSiteId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-content-app-user.json b/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-content-app-user.json index 80b47a56de7..ccfa030cd8d 100644 --- a/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-content-app-user.json +++ b/firstpartydata/tests/resolvefpd/global-and-no-bidder-fpd-site-content-app-user.json @@ -66,7 +66,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "ref": "fpdRef", "publisher": { @@ -87,7 +87,6 @@ "ext": { "testSiteExt": 123, "data": { - "id": "apnSiteId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/global-fpd-only-app.json b/firstpartydata/tests/resolvefpd/global-fpd-only-app.json index d7bd3589aee..e4d5e169986 100644 --- a/firstpartydata/tests/resolvefpd/global-fpd-only-app.json +++ b/firstpartydata/tests/resolvefpd/global-fpd-only-app.json @@ -65,7 +65,7 @@ }, "outputRequestData": { "app": { - "id": "reqAppId", + "id": "apnAppId", "publisher": { "id": "1" }, @@ -89,7 +89,6 @@ "ext": { "testAppExt": 123, "data": { - "id": "apnAppId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/global-fpd-only-site.json b/firstpartydata/tests/resolvefpd/global-fpd-only-site.json index 73f827888ed..49b8f4dcefb 100644 --- a/firstpartydata/tests/resolvefpd/global-fpd-only-site.json +++ b/firstpartydata/tests/resolvefpd/global-fpd-only-site.json @@ -73,7 +73,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "ref": "fpdRef", "publisher": { @@ -99,7 +99,6 @@ "ext": { "testSiteExt": 123, "data": { - "id": "apnSiteId", "moreFpd": { "fpd": 123 }, diff --git a/firstpartydata/tests/resolvefpd/req-and-bidder-fpd-site-content.json b/firstpartydata/tests/resolvefpd/req-and-bidder-fpd-site-content.json index f3e5fdd22bc..446e7151afa 100644 --- a/firstpartydata/tests/resolvefpd/req-and-bidder-fpd-site-content.json +++ b/firstpartydata/tests/resolvefpd/req-and-bidder-fpd-site-content.json @@ -90,7 +90,7 @@ }, "outputRequestData": { "site": { - "id": "reqSiteId", + "id": "apnSiteId", "page": "http://www.foobar.com/1234.html", "ref": "fpdRef", "publisher": { @@ -116,7 +116,6 @@ "ext": { "testSiteExt": 123, "data": { - "id": "apnSiteId", "moreFpd": { "fpd": 123 }, diff --git a/floors/enforce.go b/floors/enforce.go new file mode 100644 index 00000000000..9318c9d278e --- /dev/null +++ b/floors/enforce.go @@ -0,0 +1,260 @@ +package floors + +import ( + "errors" + "fmt" + "math/rand" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// Enforce does floors enforcement for bids from all bidders based on floors provided in request, account level floors config +func Enforce(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, account config.Account, conversions currency.Conversions) (map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []error, []*entities.PbsOrtbSeatBid) { + rejectionErrs := []error{} + + rejectedBids := []*entities.PbsOrtbSeatBid{} + + requestExt, err := bidRequestWrapper.GetRequestExt() + if err != nil { + return seatBids, []error{errors.New("Error in getting request extension")}, rejectedBids + } + + if !isPriceFloorsEnabled(account, bidRequestWrapper) { + return seatBids, nil, rejectedBids + } + + if isSignalingSkipped(requestExt) || !isValidImpBidFloorPresent(bidRequestWrapper.BidRequest.Imp) { + return seatBids, nil, rejectedBids + } + + enforceFloors := isSatisfiedByEnforceRate(requestExt, account.PriceFloors.EnforceFloorsRate, rand.Intn) + if updateEnforcePBS(enforceFloors, requestExt) { + err := bidRequestWrapper.RebuildRequest() + if err != nil { + return seatBids, []error{err}, rejectedBids + } + } + updateBidExt(bidRequestWrapper, seatBids) + if enforceFloors { + enforceDealFloors := account.PriceFloors.EnforceDealFloors && getEnforceDealsFlag(requestExt) + seatBids, rejectionErrs, rejectedBids = enforceFloorToBids(bidRequestWrapper, seatBids, conversions, enforceDealFloors) + } + return seatBids, rejectionErrs, rejectedBids +} + +// updateEnforcePBS updates prebid extension in request if enforcePBS needs to be updated +func updateEnforcePBS(enforceFloors bool, requestExt *openrtb_ext.RequestExt) bool { + updateReqExt := false + + prebidExt := requestExt.GetPrebid() + if prebidExt == nil { + prebidExt = new(openrtb_ext.ExtRequestPrebid) + } + + if prebidExt.Floors == nil { + prebidExt.Floors = new(openrtb_ext.PriceFloorRules) + } + floorExt := prebidExt.Floors + + if floorExt.Enforcement == nil { + floorExt.Enforcement = new(openrtb_ext.PriceFloorEnforcement) + } + + if floorExt.Enforcement.EnforcePBS == nil { + updateReqExt = true + floorExt.Enforcement.EnforcePBS = new(bool) + *floorExt.Enforcement.EnforcePBS = enforceFloors + } else { + oldEnforcePBS := *floorExt.Enforcement.EnforcePBS + *floorExt.Enforcement.EnforcePBS = enforceFloors && *floorExt.Enforcement.EnforcePBS + updateReqExt = oldEnforcePBS != *floorExt.Enforcement.EnforcePBS + } + + if updateReqExt { + requestExt.SetPrebid(prebidExt) + } + + return updateReqExt +} + +// updateBidExt updates bid extension for floors related details +func updateBidExt(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) { + impMap := make(map[string]*openrtb_ext.ImpWrapper, bidRequestWrapper.LenImp()) + for _, imp := range bidRequestWrapper.GetImp() { + impMap[imp.ID] = imp + } + + for _, seatBid := range seatBids { + for _, bid := range seatBid.Bids { + reqImp, ok := impMap[bid.Bid.ImpID] + if ok { + updateBidExtWithFloors(reqImp, bid, reqImp.BidFloorCur) + } + } + } +} + +// enforceFloorToBids function does floors enforcement for each bid, +// The bids returned by each partner below bid floor price are rejected and remaining eligible bids are considered for further processing +func enforceFloorToBids(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, conversions currency.Conversions, enforceDealFloors bool) (map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []error, []*entities.PbsOrtbSeatBid) { + errs := []error{} + rejectedBids := []*entities.PbsOrtbSeatBid{} + impMap := make(map[string]*openrtb_ext.ImpWrapper, bidRequestWrapper.LenImp()) + + for _, v := range bidRequestWrapper.GetImp() { + impMap[v.ID] = v + } + + for bidderName, seatBid := range seatBids { + eligibleBids := make([]*entities.PbsOrtbBid, 0, len(seatBid.Bids)) + for _, bid := range seatBid.Bids { + + reqImp, ok := impMap[bid.Bid.ImpID] + if !ok { + continue + } + + requestExt, err := bidRequestWrapper.GetRequestExt() + if err != nil { + errs = append(errs, fmt.Errorf("error in getting req extension = %v", err.Error())) + continue + } + + if isEnforcementEnabled(requestExt) { + if hasDealID(bid) && !enforceDealFloors { + eligibleBids = append(eligibleBids, bid) + continue + } + + rate, err := getCurrencyConversionRate(seatBid.Currency, reqImp.BidFloorCur, conversions) + if err != nil { + errs = append(errs, fmt.Errorf("error in rate conversion from = %s to %s with bidder %s for impression id %s and bid id %s error = %v", seatBid.Currency, reqImp.BidFloorCur, bidderName, bid.Bid.ImpID, bid.Bid.ID, err.Error())) + continue + } + + bidPrice := rate * bid.Bid.Price + if (bidPrice + floorPrecision) < reqImp.BidFloor { + rejectedBid := &entities.PbsOrtbSeatBid{ + Currency: seatBid.Currency, + Seat: seatBid.Seat, + Bids: []*entities.PbsOrtbBid{bid}, + } + rejectedBids = append(rejectedBids, rejectedBid) + continue + } + } + eligibleBids = append(eligibleBids, bid) + } + seatBids[bidderName].Bids = eligibleBids + } + return seatBids, errs, rejectedBids +} + +// isEnforcementEnabled check for floors enforcement enabled in request +func isEnforcementEnabled(requestExt *openrtb_ext.RequestExt) bool { + if floorsExt := getFloorsExt(requestExt); floorsExt != nil { + return floorsExt.GetEnforcePBS() + } + return true +} + +// isSignalingSkipped check for floors signalling is skipped due to skip rate +func isSignalingSkipped(requestExt *openrtb_ext.RequestExt) bool { + if floorsExt := getFloorsExt(requestExt); floorsExt != nil { + return floorsExt.GetFloorsSkippedFlag() + } + return false +} + +// getEnforceRateRequest returns enforceRate provided in request +func getEnforceRateRequest(requestExt *openrtb_ext.RequestExt) int { + if floorsExt := getFloorsExt(requestExt); floorsExt != nil { + return floorsExt.GetEnforceRate() + } + return 0 +} + +// getEnforceDealsFlag returns FloorDeals flag from req.ext.prebid.floors.enforcement +func getEnforceDealsFlag(requestExt *openrtb_ext.RequestExt) bool { + if floorsExt := getFloorsExt(requestExt); floorsExt != nil { + return floorsExt.GetEnforceDealsFlag() + } + return false +} + +// getFloorsExt returns req.ext.prebid.floors +func getFloorsExt(requestExt *openrtb_ext.RequestExt) *openrtb_ext.PriceFloorRules { + if requestExt != nil { + if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil { + return prebidExt.Floors + } + } + return nil +} + +// isValidImpBidFloorPresent checks if non zero imp.bidfloor is present in request +func isValidImpBidFloorPresent(imp []openrtb2.Imp) bool { + for i := range imp { + if imp[i].BidFloor > 0 { + return true + } + } + return false +} + +// isSatisfiedByEnforceRate check enforcements should be done or not based on enforceRate in config and in request +func isSatisfiedByEnforceRate(requestExt *openrtb_ext.RequestExt, configEnforceRate int, f func(int) int) bool { + requestEnforceRate := getEnforceRateRequest(requestExt) + enforceRate := f(enforceRateMax) + satisfiedByRequest := requestEnforceRate == 0 || enforceRate < requestEnforceRate + satisfiedByAccount := configEnforceRate == 0 || enforceRate < configEnforceRate + shouldEnforce := satisfiedByRequest && satisfiedByAccount + + return shouldEnforce +} + +// hasDealID checks for dealID presence in bid +func hasDealID(bid *entities.PbsOrtbBid) bool { + if bid != nil && bid.Bid != nil && bid.Bid.DealID != "" { + return true + } + return false +} + +// getCurrencyConversionRate gets conversion rate in case floor currency and seatBid currency are not same +func getCurrencyConversionRate(seatBidCur, reqImpCur string, conversions currency.Conversions) (float64, error) { + rate := 1.0 + if seatBidCur != reqImpCur { + return conversions.GetRate(seatBidCur, reqImpCur) + } else { + return rate, nil + } +} + +// updateBidExtWithFloors updates floors related details in bid extension +func updateBidExtWithFloors(reqImp *openrtb_ext.ImpWrapper, bid *entities.PbsOrtbBid, floorCurrency string) { + impExt, err := reqImp.GetImpExt() + if err != nil { + return + } + + var bidExtFloors openrtb_ext.ExtBidPrebidFloors + prebidExt := impExt.GetPrebid() + if prebidExt == nil || prebidExt.Floors == nil { + if reqImp.BidFloor > 0 { + bidExtFloors.FloorValue = reqImp.BidFloor + bidExtFloors.FloorCurrency = reqImp.BidFloorCur + bid.BidFloors = &bidExtFloors + } + } else { + bidExtFloors.FloorRule = prebidExt.Floors.FloorRule + bidExtFloors.FloorRuleValue = prebidExt.Floors.FloorRuleValue + bidExtFloors.FloorValue = prebidExt.Floors.FloorValue + bidExtFloors.FloorCurrency = floorCurrency + bid.BidFloors = &bidExtFloors + } +} diff --git a/floors/enforce_test.go b/floors/enforce_test.go new file mode 100644 index 00000000000..725ac22b193 --- /dev/null +++ b/floors/enforce_test.go @@ -0,0 +1,1224 @@ +package floors + +import ( + "encoding/json" + "errors" + "reflect" + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +type convert struct { +} + +func (c convert) GetRate(from string, to string) (float64, error) { + if from == to { + return 1, nil + } + + if from == "USD" && to == "INR" { + return 77.59, nil + } else if from == "INR" && to == "USD" { + return 0.013, nil + } + return 0, errors.New("currency conversion not supported") +} + +func (c convert) GetRates() *map[string]map[string]float64 { + return &map[string]map[string]float64{} +} + +func TestIsValidImpBidfloorPresentInRequest(t *testing.T) { + + tests := []struct { + name string + imp []openrtb2.Imp + want bool + }{ + { + imp: []openrtb2.Imp{{ID: "1234"}}, + want: false, + }, + { + imp: []openrtb2.Imp{{ID: "1234", BidFloor: 10, BidFloorCur: "USD"}}, + want: true, + }, + } + + for _, tt := range tests { + got := isValidImpBidFloorPresent(tt.imp) + assert.Equal(t, tt.want, got, tt.name) + } +} + +func TestEnforceFloorToBids(t *testing.T) { + type args struct { + bidRequestWrapper *openrtb_ext.RequestWrapper + seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + conversions currency.Conversions + enforceDealFloors bool + } + tests := []struct { + name string + args args + expEligibleBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + expErrs []error + expRejectedBids []*entities.PbsOrtbSeatBid + }{ + { + name: "Floors enforcement disabled using enforcepbs = false", + args: args{ + bidRequestWrapper: func() *openrtb_ext.RequestWrapper { + bw := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + {ID: "some-impression-id-1", BidFloor: 1.01, BidFloorCur: "USD"}, + {ID: "some-impression-id-2", BidFloor: 2.01, BidFloorCur: "USD"}, + }, + Ext: json.RawMessage(`{"prebid":{"floors":{"enforcement":{"enforcepbs":false}}}}`), + }, + } + bw.RebuildRequest() + return &bw + }(), + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: false, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + expErrs: []error{}, + }, + { + name: "Bids with price less than bidfloor", + args: args{ + bidRequestWrapper: func() *openrtb_ext.RequestWrapper { + bw := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + {ID: "some-impression-id-1", BidFloor: 1.01, BidFloorCur: "USD"}, + {ID: "some-impression-id-2", BidFloor: 2.01, BidFloorCur: "USD"}, + }, + }, + } + bw.RebuildRequest() + return &bw + }(), + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: false, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{ + { + Seat: "appnexus", + Currency: "USD", + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}}, + }, + }, + }, + expErrs: []error{}, + }, + { + name: "Bids with price less than bidfloor with floorsPrecision", + args: args{ + bidRequestWrapper: func() *openrtb_ext.RequestWrapper { + bw := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + {ID: "some-impression-id-1", BidFloor: 1, BidFloorCur: "USD"}, + {ID: "some-impression-id-2", BidFloor: 2, BidFloorCur: "USD"}, + }, + }, + } + bw.RebuildRequest() + return &bw + }(), + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 0.998, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.8, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: false, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 0.998, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{ + { + Seat: "appnexus", + Currency: "USD", + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.8, ImpID: "some-impression-id-1"}}, + }, + }, + }, + expErrs: []error{}, + }, + { + name: "Bids with different currency with enforceDealFloor true", + args: args{ + bidRequestWrapper: func() *openrtb_ext.RequestWrapper { + bw := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + {ID: "some-impression-id-1", BidFloor: 60, BidFloorCur: "INR"}, + {ID: "some-impression-id-2", BidFloor: 100, BidFloorCur: "INR"}, + }, + }, + } + bw.RebuildRequest() + return &bw + }(), + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + {Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{ + { + Seat: "appnexus", + Currency: "USD", + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}}, + }, + }, + }, + expErrs: []error{}, + }, + { + name: "Error in currency conversion", + args: args{ + bidRequestWrapper: func() *openrtb_ext.RequestWrapper { + bw := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Cur: []string{"USD"}, + Imp: []openrtb2.Imp{{ID: "some-impression-id-1", BidFloor: 1.01}}, + }, + } + bw.RebuildRequest() + return &bw + }(), + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + }, + Currency: "EUR", + }, + }, + conversions: convert{}, + enforceDealFloors: true, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{}, + Currency: "EUR", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + expErrs: []error{errors.New("error in rate conversion from = EUR to with bidder pubmatic for impression id some-impression-id-1 and bid id some-bid-1 error = currency conversion not supported")}, + }, + { + name: "Bids with invalid impression ID", + args: args{ + bidRequestWrapper: func() *openrtb_ext.RequestWrapper { + bw := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + {ID: "some-impression-id-2", BidFloor: 2.01, BidFloorCur: "USD"}, + }, + }, + } + bw.RebuildRequest() + return &bw + }(), + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-123"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: false, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "pubmatic", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + expErrs: []error{}, + }, + } + for _, tt := range tests { + seatbids, errs, rejBids := enforceFloorToBids(tt.args.bidRequestWrapper, tt.args.seatBids, tt.args.conversions, tt.args.enforceDealFloors) + assert.Equal(t, tt.expEligibleBids, seatbids, tt.name) + assert.Equal(t, tt.expErrs, errs, tt.name) + assert.Equal(t, tt.expRejectedBids, rejBids, tt.name) + } +} + +func TestEnforce(t *testing.T) { + type args struct { + bidRequestWrapper *openrtb_ext.RequestWrapper + bidRequest *openrtb2.BidRequest + seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + priceFloorsCfg config.AccountPriceFloors + conversions currency.Conversions + } + tests := []struct { + name string + args args + expEligibleBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + expErrs []error + expRejectedBids []*entities.PbsOrtbSeatBid + }{ + { + name: "Error in getting request extension", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, + BidFloor: 5.01, + BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":false,"skipped":false}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: false, EnforceFloorsRate: 100, EnforceDealFloors: true}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + expErrs: []error{errors.New("Error in getting request extension")}, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + }, + { + name: "Should not enforce floors when req.ext.prebid.floors.enabled = false", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, + BidFloor: 5.01, + BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":false}}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + }, + { + name: "Should not enforce floors is disabled in account config", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + BidFloor: 5.01, + BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":true}}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: false, EnforceFloorsRate: 100, EnforceDealFloors: true}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + }, + { + name: "Should not enforce floors when req.ext.prebid.floors.enforcement.enforcepbs = false", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + BidFloor: 5.01, + BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":false,"floordeals":false},"enabled":true,"skipped":false}}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorValue: 5.01, FloorCurrency: "USD"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + expErrs: []error{}, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + }, + { + name: "Should not enforce floors when req.ext.prebid.floors.skipped = true", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + BidFloor: 5.01, + BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":true,"skipped":true}}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{}, + }, + { + name: "Should enforce floors for deals, ext.prebid.floors.enforcement.floorDeals = true and floors enabled = true", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, BidFloor: 20.01, BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}}}, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, DealID: "deal_Id_3", ImpID: "some-impression-id-1"}}}, + Seat: "appnexus", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 0, EnforceDealFloors: true}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "appnexus", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{ + { + Seat: "pubmatic", + Currency: "USD", + Bids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorCurrency: "USD", FloorValue: 20.01}}}, + }, + { + Seat: "appnexus", + Currency: "USD", + Bids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, DealID: "deal_Id_3", ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorCurrency: "USD", FloorValue: 20.01}}}, + }, + }, + expErrs: []error{}, + }, + { + name: "Should enforce floors when imp.bidfloor provided and enforcepbs not provided", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id-1", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, BidFloor: 5.01, BidFloorCur: "USD", + }}, + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1}}}`), + }, + }, + seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}}, + }, + Seat: "appnexus", + Currency: "USD", + }, + }, + conversions: convert{}, + priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 0, EnforceDealFloors: false}, + }, + expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "pubmatic": { + Bids: []*entities.PbsOrtbBid{ + {Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorValue: 5.01, FloorCurrency: "USD"}}, + }, + Seat: "pubmatic", + Currency: "USD", + }, + "appnexus": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "appnexus", + Currency: "USD", + }, + }, + expRejectedBids: []*entities.PbsOrtbSeatBid{ + { + Seat: "appnexus", + Currency: "USD", + Bids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorValue: 5.01, FloorCurrency: "USD"}}}, + }, + }, + expErrs: []error{}, + }, + } + for _, tt := range tests { + actEligibleBids, actErrs, actRejecteBids := Enforce(tt.args.bidRequestWrapper, tt.args.seatBids, config.Account{PriceFloors: tt.args.priceFloorsCfg}, tt.args.conversions) + assert.Equal(t, tt.expErrs, actErrs, tt.name) + assert.ElementsMatch(t, tt.expRejectedBids, actRejecteBids, tt.name) + + if !reflect.DeepEqual(tt.expEligibleBids, actEligibleBids) { + assert.Failf(t, "eligible bids don't match", "Expected: %v, Got: %v", tt.expEligibleBids, actEligibleBids) + } + } +} + +func TestUpdateBidExtWithFloors(t *testing.T) { + type args struct { + reqImp *openrtb_ext.ImpWrapper + bid *entities.PbsOrtbBid + floorCurrency string + } + tests := []struct { + name string + expBidFloor *openrtb_ext.ExtBidPrebidFloors + args args + }{ + { + name: "Empty prebid extension in imp.ext", + args: args{ + reqImp: &openrtb_ext.ImpWrapper{ + Imp: &openrtb2.Imp{BidFloor: 10, BidFloorCur: "USD"}, + }, + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Price: 10.10, + AdM: "Adm", + }, + }, + floorCurrency: "USD", + }, + expBidFloor: &openrtb_ext.ExtBidPrebidFloors{ + FloorValue: 10, + FloorCurrency: "USD", + }, + }, + { + name: "Valid prebid extension in imp.ext", + args: args{ + reqImp: &openrtb_ext.ImpWrapper{Imp: &openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}, Ext: []byte(`{"prebid":{"floors":{"floorrule":"test|123|xyz","floorrulevalue":5.5,"floorvalue":5.5}}}`)}}, + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Price: 10.10, + AdM: "Adm", + }, + }, + floorCurrency: "USD", + }, + expBidFloor: &openrtb_ext.ExtBidPrebidFloors{ + FloorRule: "test|123|xyz", + FloorRuleValue: 5.5, + FloorValue: 5.5, + FloorCurrency: "USD", + }, + }, + } + for _, tt := range tests { + updateBidExtWithFloors(tt.args.reqImp, tt.args.bid, tt.args.floorCurrency) + assert.Equal(t, tt.expBidFloor, tt.args.bid.BidFloors, tt.name) + } +} + +func TestIsEnforcementEnabledForRequest(t *testing.T) { + tests := []struct { + name string + reqExt *openrtb_ext.RequestExt + want bool + }{ + { + name: "Req.ext not provided", + reqExt: func() *openrtb_ext.RequestExt { + return &openrtb_ext.RequestExt{} + }(), + want: true, + }, + { + name: "Req.ext provided EnforcePBS = false", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: ptrutil.ToPtr(false), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: false, + }, + { + name: "Req.ext provided EnforcePBS = true", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: true, + }, + } + for _, tt := range tests { + got := isEnforcementEnabled(tt.reqExt) + assert.Equal(t, tt.want, got, tt.name) + } +} + +func TestIsSignalingSkipped(t *testing.T) { + tests := []struct { + name string + reqExt *openrtb_ext.RequestExt + want bool + }{ + { + name: "Req.ext nil", + reqExt: func() *openrtb_ext.RequestExt { + return nil + }(), + want: false, + }, + { + name: "Req.ext provided without prebid ext", + reqExt: func() *openrtb_ext.RequestExt { + return &openrtb_ext.RequestExt{} + }(), + want: false, + }, + { + name: "Req.ext provided without Floors in prebid ext", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{} + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: false, + }, + { + name: "Req.ext provided Skipped = true", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Skipped: ptrutil.ToPtr(true), + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: true, + }, + { + name: "Req.ext provided Skipped = false", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Skipped: ptrutil.ToPtr(false), + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: false, + }, + } + for _, tt := range tests { + got := isSignalingSkipped(tt.reqExt) + assert.Equal(t, tt.want, got, tt.name) + } +} + +func TestGetEnforceRateRequest(t *testing.T) { + tests := []struct { + name string + reqExt *openrtb_ext.RequestExt + want int + }{ + { + name: "Req.ext not provided", + reqExt: func() *openrtb_ext.RequestExt { + return &openrtb_ext.RequestExt{} + }(), + want: 0, + }, + { + name: "Req.ext.prebid.floors provided with EnforceRate = 0", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforceRate: 0, + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: 0, + }, + { + name: "Req.ext.prebid.floors provided with EnforceRate = 50", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforceRate: 50, + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: 50, + }, + { + name: "Req.ext.prebid.floors provided with EnforceRate = 100", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforceRate: 100, + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: 100, + }, + } + for _, tt := range tests { + got := getEnforceRateRequest(tt.reqExt) + assert.Equal(t, tt.want, got, tt.name) + } +} + +func TestGetEnforceDealsFlag(t *testing.T) { + tests := []struct { + name string + reqExt *openrtb_ext.RequestExt + want bool + }{ + { + name: "Req.ext not provided", + reqExt: func() *openrtb_ext.RequestExt { + return &openrtb_ext.RequestExt{} + }(), + want: false, + }, + { + name: "Req.ext.prebid.floors provided, enforceDeals not provided", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{}, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: false, + }, + { + name: "Req.ext.prebid.floors provided with enforceDeals = false", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + FloorDeals: ptrutil.ToPtr(false), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: false, + }, + { + name: "Req.ext.prebid.floors provided with enforceDeals = true", + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + FloorDeals: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + want: true, + }, + } + for _, tt := range tests { + got := getEnforceDealsFlag(tt.reqExt) + assert.Equal(t, tt.want, got, tt.name) + } +} + +func TestIsSatisfiedByEnforceRate(t *testing.T) { + type args struct { + reqExt *openrtb_ext.RequestExt + configEnforceRate int + f func(int) int + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "With EnforceRate = 50", + args: args{ + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforceRate: 50, + EnforcePBS: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + configEnforceRate: 100, + f: func(n int) int { + return n + }, + }, + want: false, + }, + { + name: "With EnforceRate = 100", + args: args{ + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforceRate: 100, + EnforcePBS: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + configEnforceRate: 100, + f: func(n int) int { + return n - 1 + }, + }, + want: true, + }, + { + name: "With configEnforceRate = 0", + args: args{ + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforceRate: 0, + EnforcePBS: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + configEnforceRate: 0, + f: func(n int) int { + return n - 1 + }, + }, + want: true, + }, + } + for _, tt := range tests { + got := isSatisfiedByEnforceRate(tt.args.reqExt, tt.args.configEnforceRate, tt.args.f) + assert.Equal(t, tt.want, got, tt.name) + } +} + +func TestUpdateEnforcePBS(t *testing.T) { + type args struct { + enforceFloors bool + reqExt *openrtb_ext.RequestExt + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Enforce PBS is true in request and to be updated = true", + args: args{ + enforceFloors: true, + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + }, + want: false, + }, + { + name: "Enforce PBS is false in request and to be updated = true", + args: args{ + enforceFloors: true, + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: ptrutil.ToPtr(false), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + }, + want: false, + }, + { + name: "Enforce PBS is true in request and to be updated = false", + args: args{ + enforceFloors: false, + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{ + Floors: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: ptrutil.ToPtr(true), + }, + }, + } + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + }, + want: true, + }, + { + name: "empty prebid ext and to be updated = false", + args: args{enforceFloors: false, + reqExt: func() *openrtb_ext.RequestExt { + return &openrtb_ext.RequestExt{} + }(), + }, + want: true, + }, + { + name: "empty prebid ext and to be updated = true", + args: args{enforceFloors: true, + reqExt: func() *openrtb_ext.RequestExt { + reqExt := openrtb_ext.RequestExt{} + prebidExt := openrtb_ext.ExtRequestPrebid{} + reqExt.SetPrebid(&prebidExt) + return &reqExt + }(), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateEnforcePBS(tt.args.enforceFloors, tt.args.reqExt) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} diff --git a/floors/floors.go b/floors/floors.go index efed0d11590..52591915058 100644 --- a/floors/floors.go +++ b/floors/floors.go @@ -16,14 +16,16 @@ type Price struct { } const ( - defaultDelimiter string = "|" - catchAll string = "*" - skipRateMin int = 0 - skipRateMax int = 100 - modelWeightMax int = 100 - modelWeightMin int = 1 - enforceRateMin int = 0 - enforceRateMax int = 100 + defaultCurrency string = "USD" + defaultDelimiter string = "|" + catchAll string = "*" + skipRateMin int = 0 + skipRateMax int = 100 + modelWeightMax int = 100 + modelWeightMin int = 1 + enforceRateMin int = 0 + enforceRateMax int = 100 + floorPrecision float64 = 0.01 ) // EnrichWithPriceFloors checks for floors enabled in account and request and selects floors data from dynamic fetched if present @@ -34,7 +36,7 @@ func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, accoun return []error{errors.New("Empty bidrequest")} } - if isPriceFloorsDisabled(account, bidRequestWrapper) { + if !isPriceFloorsEnabled(account, bidRequestWrapper) { return []error{errors.New("Floors feature is disabled at account or in the request")} } @@ -112,25 +114,25 @@ func roundToFourDecimals(in float64) float64 { return math.Round(in*10000) / 10000 } -// isPriceFloorsDisabled check for floors are disabled at account or request level -func isPriceFloorsDisabled(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper) bool { - return isPriceFloorsDisabledForAccount(account) || isPriceFloorsDisabledForRequest(bidRequestWrapper) +// isPriceFloorsEnabled check for floors are enabled at account and request level +func isPriceFloorsEnabled(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper) bool { + return isPriceFloorsEnabledForAccount(account) && isPriceFloorsEnabledForRequest(bidRequestWrapper) } -// isPriceFloorsDisabledForAccount check for floors are disabled at account -func isPriceFloorsDisabledForAccount(account config.Account) bool { - return !account.PriceFloors.Enabled +// isPriceFloorsEnabledForAccount check for floors enabled flag in account config +func isPriceFloorsEnabledForAccount(account config.Account) bool { + return account.PriceFloors.Enabled } -// isPriceFloorsDisabledForRequest check for floors are disabled at request -func isPriceFloorsDisabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) bool { +// isPriceFloorsEnabledForRequest check for floors are enabled flag in request +func isPriceFloorsEnabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) bool { requestExt, err := bidRequestWrapper.GetRequestExt() if err == nil { - if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil && !prebidExt.Floors.GetEnabled() { - return true + if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil { + return prebidExt.Floors.GetEnabled() } } - return false + return true } // resolveFloors does selection of floors fields from request data and dynamic fetched data if dynamic fetch is enabled @@ -200,10 +202,11 @@ func updateFloorsInRequest(bidRequestWrapper *openrtb_ext.RequestWrapper, priceF requestExt, err := bidRequestWrapper.GetRequestExt() if err == nil { prebidExt := requestExt.GetPrebid() - if prebidExt != nil { - prebidExt.Floors = priceFloors - requestExt.SetPrebid(prebidExt) - bidRequestWrapper.RebuildRequest() + if prebidExt == nil { + prebidExt = &openrtb_ext.ExtRequestPrebid{} } + prebidExt.Floors = priceFloors + requestExt.SetPrebid(prebidExt) + bidRequestWrapper.RebuildRequest() } } diff --git a/floors/floors_test.go b/floors/floors_test.go index e5a4a3296c0..7f27e1a26d5 100644 --- a/floors/floors_test.go +++ b/floors/floors_test.go @@ -671,3 +671,69 @@ func TestCreateFloorsFrom(t *testing.T) { }) } } + +func TestIsPriceFloorsEnabled(t *testing.T) { + type args struct { + account config.Account + bidRequestWrapper *openrtb_ext.RequestWrapper + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Disabled in account and req", + args: args{ + account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: false}}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": false} }}`), + }, + }, + }, + want: false, + }, + { + name: "Enabled in account and req", + args: args{ + account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: true}}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": true} }}`), + }, + }, + }, + want: true, + }, + { + name: "disabled in account and enabled req", + args: args{ + account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: false}}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": true} }}`), + }, + }, + }, + want: false, + }, + { + name: "Enabled in account and disabled in req", + args: args{ + account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: true}}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": false} }}`)}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isPriceFloorsEnabled(tt.args.account, tt.args.bidRequestWrapper) + assert.Equal(t, got, tt.want, tt.name) + }) + } +} diff --git a/floors/rule.go b/floors/rule.go index 101239e9cf8..f5f74cb6acf 100644 --- a/floors/rule.go +++ b/floors/rule.go @@ -51,7 +51,7 @@ func getFloorCurrency(floorExt *openrtb_ext.PriceFloorRules) string { } if len(floorCur) == 0 { - floorCur = "USD" + floorCur = defaultCurrency } return floorCur diff --git a/gdpr/consent_parser.go b/gdpr/consent_parser.go new file mode 100644 index 00000000000..3a354886036 --- /dev/null +++ b/gdpr/consent_parser.go @@ -0,0 +1,67 @@ +package gdpr + +import ( + "errors" + "fmt" + + "github.com/prebid/go-gdpr/api" + "github.com/prebid/go-gdpr/vendorconsent" + tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2" +) + +// parsedConsent represents a parsed consent string containing notable version information and a convenient +// metadata object that allows easy examination of encoded purpose and vendor information +type parsedConsent struct { + encodingVersion uint8 + listVersion uint16 + specVersion uint16 + consentMeta tcf2.ConsentMetadata +} + +// parseConsent parses and validates the specified consent string returning an instance of parsedConsent +func parseConsent(consent string) (*parsedConsent, error) { + pc, err := vendorconsent.ParseString(consent) + if err != nil { + return nil, &ErrorMalformedConsent{ + Consent: consent, + Cause: err, + } + } + if err = validateVersions(pc); err != nil { + return nil, &ErrorMalformedConsent{ + Consent: consent, + Cause: err, + } + } + cm, ok := pc.(tcf2.ConsentMetadata) + if !ok { + err = errors.New("Unable to access TCF2 parsed consent") + return nil, err + } + return &parsedConsent{ + encodingVersion: pc.Version(), + listVersion: pc.VendorListVersion(), + specVersion: getSpecVersion(pc.TCFPolicyVersion()), + consentMeta: cm, + }, nil +} + +// validateVersions ensures that certain version fields in the consent string contain valid values. +// An error is returned if at least one of them is invalid +func validateVersions(pc api.VendorConsents) (err error) { + version := pc.Version() + if version != 2 { + return fmt.Errorf("invalid encoding format version: %d", version) + } + return +} + +// getSpecVersion looks at the TCF policy version and determines the corresponding GVL specification +// version that should be used to calculate legal basis. A zero value is returned if the policy version +// is invalid +func getSpecVersion(policyVersion uint8) uint16 { + if policyVersion >= 4 { + return 3 + } + return 2 +} diff --git a/gdpr/consent_parser_test.go b/gdpr/consent_parser_test.go new file mode 100644 index 00000000000..4708f689f05 --- /dev/null +++ b/gdpr/consent_parser_test.go @@ -0,0 +1,160 @@ +package gdpr + +import ( + "errors" + "testing" + "time" + + "github.com/prebid/go-gdpr/consentconstants" + + "github.com/stretchr/testify/assert" +) + +func TestParseConsent(t *testing.T) { + validTCF1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY" + validTCF2Consent := "CPuKGCPPuKGCPNEAAAENCZCAAAAAAAAAAAAAAAAAAAAA" + + tests := []struct { + name string + consent string + expectedEncodingVersion uint8 + expectedListVersion uint16 + expectedSpecVersion uint16 + expectedError error + }{ + + { + name: "valid_consent_with_encoding_version_2", + consent: validTCF2Consent, + expectedEncodingVersion: 2, + expectedListVersion: 153, + expectedSpecVersion: 2, + }, + { + name: "invalid_consent_parsing_error", + consent: "", + expectedError: &ErrorMalformedConsent{ + Consent: "", + Cause: consentconstants.ErrEmptyDecodedConsent, + }, + }, + { + name: "invalid_consent_version_validation_error", + consent: validTCF1Consent, + expectedError: &ErrorMalformedConsent{ + Consent: validTCF1Consent, + Cause: errors.New("invalid encoding format version: 1"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsedConsent, err := parseConsent(tt.consent) + + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + assert.Nil(t, parsedConsent) + } else { + assert.NoError(t, err) + assert.NotNil(t, parsedConsent) + assert.Equal(t, uint8(2), parsedConsent.encodingVersion) + assert.Equal(t, uint16(153), parsedConsent.listVersion) + assert.Equal(t, uint16(2), parsedConsent.specVersion) + assert.Equal(t, tt.expectedEncodingVersion, parsedConsent.consentMeta.Version()) + assert.Equal(t, tt.expectedListVersion, parsedConsent.consentMeta.VendorListVersion()) + } + }) + } +} + +func TestValidateVersions(t *testing.T) { + tests := []struct { + name string + version uint8 + expectedError error + }{ + { + name: "valid_consent_version=2", + version: 2, + }, + { + name: "invalid_consent_version<2", + version: 1, + expectedError: errors.New("invalid encoding format version: 1"), + }, + { + name: "invalid_consent_version>2", + version: 3, + expectedError: errors.New("invalid encoding format version: 3"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mcs := mockConsentString{ + version: tt.version, + } + err := validateVersions(&mcs) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestGetSpecVersion(t *testing.T) { + tests := []struct { + name string + policyVersion uint8 + expectedSpecVersion uint16 + }{ + { + name: "policy_version_0_gives_spec_version_2", + policyVersion: 0, + expectedSpecVersion: 2, + }, + { + name: "policy_version_3_gives_spec_version_2", + policyVersion: 3, + expectedSpecVersion: 2, + }, + { + name: "policy_version_4_gives_spec_version_3", + policyVersion: 4, + expectedSpecVersion: 3, + }, + { + name: "policy_version_5_gives_spec_version_3", + policyVersion: 5, + expectedSpecVersion: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + specVersion := getSpecVersion(tt.policyVersion) + assert.Equal(t, tt.expectedSpecVersion, specVersion) + }) + } +} + +type mockConsentString struct { + version uint8 + policyVersion uint8 +} + +func (mcs *mockConsentString) Version() uint8 { return mcs.version } +func (mcs *mockConsentString) Created() time.Time { return time.Time{} } +func (mcs *mockConsentString) LastUpdated() time.Time { return time.Time{} } +func (mcs *mockConsentString) CmpID() uint16 { return 0 } +func (mcs *mockConsentString) CmpVersion() uint16 { return 0 } +func (mcs *mockConsentString) ConsentScreen() uint8 { return 0 } +func (mcs *mockConsentString) ConsentLanguage() string { return "" } +func (mcs *mockConsentString) VendorListVersion() uint16 { return 0 } +func (mcs *mockConsentString) TCFPolicyVersion() uint8 { return mcs.policyVersion } +func (mcs *mockConsentString) MaxVendorID() uint16 { return 0 } +func (mcs *mockConsentString) PurposeAllowed(id consentconstants.Purpose) bool { return false } +func (mcs *mockConsentString) VendorConsent(id uint16) bool { return false } diff --git a/gdpr/full_enforcement.go b/gdpr/full_enforcement.go index b86f7a3ff88..eefa28d5499 100644 --- a/gdpr/full_enforcement.go +++ b/gdpr/full_enforcement.go @@ -67,6 +67,9 @@ func (fe *FullEnforcement) applyEnforceOverrides(overrides Overrides) (enforcePu // must declare the purpose as either consent or flex and the user must consent in accordance with // the purpose configs. func (fe *FullEnforcement) consentEstablished(consent tcf2.ConsentMetadata, vi VendorInfo, enforcePurpose bool, enforceVendors bool) bool { + if vi.vendor == nil { + return false + } if !vi.vendor.Purpose(fe.cfg.PurposeID) { return false } @@ -84,6 +87,9 @@ func (fe *FullEnforcement) consentEstablished(consent tcf2.ConsentMetadata, vi V // established, the vendor must declare the purpose as either legit interest or flex and the user // must have been provided notice for the legit interest basis in accordance with the purpose configs. func (fe *FullEnforcement) legitInterestEstablished(consent tcf2.ConsentMetadata, vi VendorInfo, enforcePurpose bool, enforceVendors bool) bool { + if vi.vendor == nil { + return false + } if !vi.vendor.LegitimateInterest(fe.cfg.PurposeID) { return false } diff --git a/gdpr/full_enforcement_test.go b/gdpr/full_enforcement_test.go index e8ccd1e4c0c..61f6e8b8520 100644 --- a/gdpr/full_enforcement_test.go +++ b/gdpr/full_enforcement_test.go @@ -898,6 +898,66 @@ func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) { } } +func TestLegalBasisWithoutVendor(t *testing.T) { + P1P2P3PurposeConsent := "CPfCRQAPfCRQAAAAAAENCgCAAOAAAAAAAAAAAAAAAAAA" + tests := []struct { + name string + config purposeConfig + wantResult bool + }{ + { + name: "enforce_purpose_&_vendors_off", + config: purposeConfig{ + EnforcePurpose: false, + EnforceVendors: false, + }, + wantResult: true, + }, + { + name: "enforce_purpose_on,_bidder_is_a_vendor_exception", + config: purposeConfig{ + EnforcePurpose: true, + EnforceVendors: false, + VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + }, + wantResult: true, + }, + { + name: "enforce_purpose_on", + config: purposeConfig{ + EnforcePurpose: true, + EnforceVendors: false, + }, + wantResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // convert consent string to TCF2 object + parsedConsent, err := vendorconsent.ParseString(P1P2P3PurposeConsent) + if err != nil { + t.Fatalf("Failed to parse consent %s\n", P1P2P3PurposeConsent) + } + consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata) + if !ok { + t.Fatalf("Failed to convert consent %s\n", P1P2P3PurposeConsent) + } + + vendorInfo := VendorInfo{ + vendorID: 32, + vendor: nil, + } + + enforcer := FullEnforcement{cfg: tt.config} + enforcer.cfg.PurposeID = consentconstants.Purpose(3) + + result := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{}) + assert.Equal(t, tt.wantResult, result) + }) + } +} + func getVendorList(t *testing.T) vendorlist.VendorList { GVL := makeVendorList() diff --git a/gdpr/gdpr_test.go b/gdpr/gdpr_test.go index 138da5d9d38..1e56c5fdede 100644 --- a/gdpr/gdpr_test.go +++ b/gdpr/gdpr_test.go @@ -43,7 +43,7 @@ func TestNewPermissions(t *testing.T) { HostVendorID: tt.hostVendorID, } vendorIDs := map[openrtb_ext.BidderName]uint16{} - vendorListFetcher := func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { + vendorListFetcher := func(ctx context.Context, specVersion, listVersion uint16) (vendorlist.VendorList, error) { return nil, nil } diff --git a/gdpr/impl.go b/gdpr/impl.go index 9f9b47cd766..cc88a1fd3c6 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -2,12 +2,9 @@ package gdpr import ( "context" - "errors" - "fmt" "github.com/prebid/go-gdpr/api" "github.com/prebid/go-gdpr/consentconstants" - "github.com/prebid/go-gdpr/vendorconsent" tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2" "github.com/prebid/prebid-server/openrtb_ext" ) @@ -63,53 +60,27 @@ func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCo if _, ok := p.nonStandardPublishers[p.publisherID]; ok { return AllowAll, nil } - if p.gdprSignal != SignalYes { return AllowAll, nil } - if p.consent == "" { return p.defaultPermissions(), nil } - - // note the bidder here is guaranteed to be enabled - vendorID, vendorFound := p.resolveVendorID(bidderCoreName, bidder) - basicEnforcementVendors := p.cfg.BasicEnforcementVendors() - _, weakVendorEnforcement := basicEnforcementVendors[string(bidder)] - - if !vendorFound && !weakVendorEnforcement { - return DenyAll, nil - } - - parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, p.consent) + pc, err := parseConsent(p.consent) if err != nil { return p.defaultPermissions(), err } - - // vendor will be nil if not a valid TCF2 consent string - if vendor == nil { - if weakVendorEnforcement && parsedConsent.Version() == 2 { - vendor = vendorTrue{} - } else { - return p.defaultPermissions(), nil - } - } - - if !p.cfg.IsEnabled() { - return AllowBidRequestOnly, nil - } - - consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata) - if !ok { - err = fmt.Errorf("Unable to access TCF2 parsed consent") + vendorID, _ := p.resolveVendorID(bidderCoreName, bidder) + vendor, err := p.getVendor(ctx, vendorID, *pc) + if err != nil { return p.defaultPermissions(), err } - vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor} + permissions = AuctionPermissions{} - permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, consentMeta, vendorInfo) - permissions.PassGeo = p.allowGeo(bidderCoreName, consentMeta, vendor) - permissions.PassID = p.allowID(bidderCoreName, consentMeta, vendorInfo) + permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, pc.consentMeta, vendorInfo) + permissions.PassGeo = p.allowGeo(bidderCoreName, pc.consentMeta, vendor) + permissions.PassID = p.allowID(bidderCoreName, pc.consentMeta, vendorInfo) return permissions, nil } @@ -148,34 +119,28 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, bidder if p.consent == "" { return false, nil } - - parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, p.consent) + pc, err := parseConsent(p.consent) if err != nil { return false, err } - - if vendor == nil { + vendor, err := p.getVendor(ctx, vendorID, *pc) + if err != nil { return false, nil } + vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor} if !p.cfg.PurposeEnforced(consentconstants.Purpose(1)) { return true, nil } - consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata) - if !ok { - err := errors.New("Unable to access TCF2 parsed consent") - return false, err - } - if p.cfg.PurposeOneTreatmentEnabled() && consentMeta.PurposeOneTreatment() { + if p.cfg.PurposeOneTreatmentEnabled() && pc.consentMeta.PurposeOneTreatment() { return p.cfg.PurposeOneTreatmentAccessAllowed(), nil } purpose := consentconstants.Purpose(1) enforcer := p.purposeEnforcerBuilder(purpose, bidder) - vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor} - if enforcer.LegalBasis(vendorInfo, bidder, consentMeta, Overrides{blockVendorExceptions: !vendorException}) { + if enforcer.LegalBasis(vendorInfo, bidder, pc.consentMeta, Overrides{blockVendorExceptions: !vendorException}) { return true, nil } return false, nil @@ -205,7 +170,7 @@ func (p *permissionsImpl) allowGeo(bidder openrtb_ext.BidderName, consentMeta tc basicEnforcementVendors := p.cfg.BasicEnforcementVendors() _, weakVendorEnforcement := basicEnforcementVendors[string(bidder)] - return consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialFeature(1) || weakVendorEnforcement) + return consentMeta.SpecialFeatureOptIn(1) && ((vendor != nil && vendor.SpecialFeature(1)) || weakVendorEnforcement) } // allowID computes the pass user ID activity legal basis for a given bidder using the enforcement algorithms @@ -229,29 +194,13 @@ func (p *permissionsImpl) allowID(bidder openrtb_ext.BidderName, consentMeta tcf return false } -// parseVendor parses the consent string and fetches the specified vendor's information from the GVL -func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) { - parsedConsent, err = vendorconsent.ParseString(consent) - if err != nil { - err = &ErrorMalformedConsent{ - Consent: consent, - Cause: err, - } - return - } - - version := parsedConsent.Version() - if version != 2 { - return - } - - vendorList, err := p.fetchVendorList(ctx, parsedConsent.VendorListVersion()) +// getVendor retrieves the GVL vendor information for a particular bidder +func (p *permissionsImpl) getVendor(ctx context.Context, vendorID uint16, pc parsedConsent) (api.Vendor, error) { + vendorList, err := p.fetchVendorList(ctx, pc.specVersion, pc.listVersion) if err != nil { - return + return nil, err } - - vendor = vendorList.Vendor(vendorID) - return + return vendorList.Vendor(vendorID), nil } // AllowHostCookies represents a GDPR permissions policy with host cookie syncing always allowed @@ -276,25 +225,3 @@ func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.B func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) { return AllowAll, nil } - -// vendorTrue claims everything. -type vendorTrue struct{} - -func (v vendorTrue) Purpose(purposeID consentconstants.Purpose) bool { - return true -} -func (v vendorTrue) PurposeStrict(purposeID consentconstants.Purpose) bool { - return true -} -func (v vendorTrue) LegitimateInterest(purposeID consentconstants.Purpose) bool { - return true -} -func (v vendorTrue) LegitimateInterestStrict(purposeID consentconstants.Purpose) bool { - return true -} -func (v vendorTrue) SpecialFeature(featureID consentconstants.SpecialFeature) (hasSpecialFeature bool) { - return true -} -func (v vendorTrue) SpecialPurpose(purposeID consentconstants.Purpose) (hasSpecialPurpose bool) { - return true -} diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 3512a271061..835a580f6e2 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -81,8 +81,10 @@ func TestAllowedSyncs(t *testing.T) { vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 1: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -127,8 +129,10 @@ func TestProhibitedPurposes(t *testing.T) { vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 1: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -174,8 +178,10 @@ func TestProhibitedVendors(t *testing.T) { vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 1: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -315,8 +321,10 @@ func TestAllowActivities(t *testing.T) { vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 1: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), } @@ -334,6 +342,109 @@ func TestAllowActivities(t *testing.T) { } } +func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) { + bidderWithoutGVLID := openrtb_ext.BidderPangle + purpose2Consent := "CPuDXznPuDXznMOAAAENCZCAAEAAAAAAAAAAAAAAAAAA" + noPurposeConsent := "CPuDXznPuDXznMOAAAENCZCAAAAAAAAAAAAAAAAAAAAA" + + tests := []struct { + name string + enforceAlgoID config.TCF2EnforcementAlgo + vendorExceptions map[openrtb_ext.BidderName]struct{} + basicEnforcementVendors map[string]struct{} + consent string + allowBidRequest bool + passID bool + }{ + { + name: "full_enforcement_no_exceptions_user_consents_to_purpose_2", + enforceAlgoID: config.TCF2FullEnforcement, + consent: purpose2Consent, + }, + { + name: "full_enforcement_vendor_exception_user_consents_to_purpose_2", + enforceAlgoID: config.TCF2FullEnforcement, + vendorExceptions: map[openrtb_ext.BidderName]struct{}{bidderWithoutGVLID: {}}, + consent: purpose2Consent, + allowBidRequest: true, + passID: true, + }, + { + name: "basic_enforcement_no_exceptions_user_consents_to_purpose_2", + consent: purpose2Consent, + }, + { + name: "basic_enforcement_vendor_exception_user_consents_to_purpose_2", + vendorExceptions: map[openrtb_ext.BidderName]struct{}{bidderWithoutGVLID: {}}, + consent: purpose2Consent, + allowBidRequest: true, + passID: true, + }, + { + name: "full_enforcement_soft_vendor_exception_user_consents_to_purpose_2", // allow bid request and pass ID + enforceAlgoID: config.TCF2FullEnforcement, + basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}}, + consent: purpose2Consent, + allowBidRequest: true, + passID: true, + }, + { + name: "basic_enforcement_soft_vendor_exception_user_consents_to_purpose_2", // allow bid request and pass ID + enforceAlgoID: config.TCF2BasicEnforcement, + basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}}, + consent: purpose2Consent, + allowBidRequest: true, + passID: true, + }, + { + name: "full_enforcement_soft_vendor_exception_user_consents_to_purpose_4", + enforceAlgoID: config.TCF2FullEnforcement, + basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}}, + consent: noPurposeConsent, + allowBidRequest: false, + passID: false, + }, + { + name: "basic_enforcement_soft_vendor_exception_user_consents_to_purpose_4", + enforceAlgoID: config.TCF2BasicEnforcement, + basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}}, + consent: noPurposeConsent, + allowBidRequest: false, + passID: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tcf2AggConfig := allPurposesEnabledTCF2Config() + tcf2AggConfig.AccountConfig.BasicEnforcementVendorsMap = tt.basicEnforcementVendors + tcf2AggConfig.HostConfig.Purpose2.VendorExceptionMap = tt.vendorExceptions + tcf2AggConfig.HostConfig.Purpose2.EnforceAlgoID = tt.enforceAlgoID + tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(2)] = &tcf2AggConfig.HostConfig.Purpose2 + + perms := permissionsImpl{ + cfg: &tcf2AggConfig, + consent: tt.consent, + gdprSignal: SignalYes, + hostVendorID: 2, + nonStandardPublishers: map[string]struct{}{}, + vendorIDs: map[openrtb_ext.BidderName]uint16{}, + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 153: parseVendorListDataV2(t, MarshalVendorList(vendorList{GVLSpecificationVersion: 2, VendorListVersion: 153, Vendors: map[string]*vendor{}})), + }, + }), + purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), + } + + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), bidderWithoutGVLID, bidderWithoutGVLID) + assert.NoError(t, err) + assert.Equal(t, tt.allowBidRequest, permissions.AllowBidRequest) + assert.Equal(t, tt.passID, permissions.PassID) + }) + } +} + func buildVendorList34() vendorList { return vendorList{ VendorListVersion: 2, @@ -444,9 +555,11 @@ func TestAllowActivitiesGeoAndID(t *testing.T) { openrtb_ext.BidderOpenx: 20, openrtb_ext.BidderAudienceNetwork: 55, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), - 74: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + 74: parseVendorListDataV2(t, vendorListData), + }, }), gdprSignal: SignalYes, } @@ -569,8 +682,10 @@ func TestAllowActivitiesWhitelist(t *testing.T) { openrtb_ext.BidderPubmatic: 6, openrtb_ext.BidderRubicon: 8, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), aliasGVLIDs: map[string]uint16{}, @@ -598,8 +713,10 @@ func TestAllowActivitiesPubRestrict(t *testing.T) { openrtb_ext.BidderPubmatic: 32, openrtb_ext.BidderRubicon: 8, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 15: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 15: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -671,8 +788,10 @@ func TestAllowSync(t *testing.T) { openrtb_ext.BidderPubmatic: 6, openrtb_ext.BidderRubicon: 8, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -705,8 +824,10 @@ func TestProhibitedPurposeSync(t *testing.T) { openrtb_ext.BidderPubmatic: 6, openrtb_ext.BidderRubicon: 8, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -737,8 +858,10 @@ func TestProhibitedVendorSync(t *testing.T) { openrtb_ext.BidderRubicon: 8, openrtb_ext.BidderOpenx: 10, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), gdprSignal: SignalYes, @@ -765,18 +888,18 @@ func parseVendorListDataV2(t *testing.T, data string) vendorlist.VendorList { return parsed } -func listFetcher(lists map[uint16]vendorlist.VendorList) func(context.Context, uint16) (vendorlist.VendorList, error) { - return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { - data, ok := lists[id] - if ok { - return data, nil - } else { - return nil, fmt.Errorf("vendorlist id=%d not found", id) +func listFetcher(specVersionLists map[uint16]map[uint16]vendorlist.VendorList) func(context.Context, uint16, uint16) (vendorlist.VendorList, error) { + return func(ctx context.Context, specVersion, listVersion uint16) (vendorlist.VendorList, error) { + if lists, ok := specVersionLists[specVersion]; ok { + if data, ok := lists[listVersion]; ok { + return data, nil + } } + return nil, fmt.Errorf("spec version %d vendor list %d not found", specVersion, listVersion) } } -func failedListFetcher(ctx context.Context, id uint16) (vendorlist.VendorList, error) { +func failedListFetcher(ctx context.Context, specVersion, listVersion uint16) (vendorlist.VendorList, error) { return nil, errors.New("vendor list can't be fetched") } @@ -957,8 +1080,10 @@ func TestAllowActivitiesBidRequests(t *testing.T) { openrtb_ext.BidderPubmatic: 6, openrtb_ext.BidderRubicon: 8, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), aliasGVLIDs: td.aliasGVLIDs, consent: td.consent, @@ -984,28 +1109,6 @@ func TestAllowActivitiesBidRequests(t *testing.T) { } } -func TestTCF1Consent(t *testing.T) { - bidderAllowedByConsent := openrtb_ext.BidderAppnexus - tcf1Consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA" - - perms := permissionsImpl{ - cfg: &tcf2Config{}, - vendorIDs: map[openrtb_ext.BidderName]uint16{ - openrtb_ext.BidderAppnexus: 2, - }, - aliasGVLIDs: map[string]uint16{}, - consent: tcf1Consent, - gdprSignal: SignalYes, - } - - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), bidderAllowedByConsent, bidderAllowedByConsent) - - assert.Nil(t, err, "TCF1 consent - no error returned") - assert.Equal(t, true, permissions.AllowBidRequest, "TCF1 consent - bid request not allowed") - assert.Equal(t, true, permissions.PassGeo, "TCF1 consent - passing geo not allowed") - assert.Equal(t, false, permissions.PassID, "TCF1 consent - passing id not allowed") -} - func TestAllowActivitiesVendorException(t *testing.T) { noPurposeOrVendorConsentAndPubRestrictsP2 := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAACEAAgAgAA" noPurposeOrVendorConsentAndPubRestrictsNone := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAAA" @@ -1073,8 +1176,10 @@ func TestAllowActivitiesVendorException(t *testing.T) { vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 32, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), aliasGVLIDs: map[string]uint16{}, consent: td.consent, @@ -1139,8 +1244,10 @@ func TestBidderSyncAllowedVendorException(t *testing.T) { vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 32, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 34: parseVendorListDataV2(t, vendorListData), + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 34: parseVendorListDataV2(t, vendorListData), + }, }), consent: td.consent, gdprSignal: SignalYes, @@ -1217,3 +1324,90 @@ func TestDefaultPermissions(t *testing.T) { assert.Equal(t, result, tt.wantPermissions, tt.description) } } + +func TestVendorListSelection(t *testing.T) { + policyVersion3WithVendor2AndPurpose1Consent := "CPGWbY_PGWbY_GYAAAENABDAAIAAAAAAAAAAACEAAAAA" + policyVersion4WithVendor2AndPurpose1Consent := "CPGWbY_PGWbY_GYAAAENABEAAIAAAAAAAAAAACEAAAAA" + + specVersion2vendorListData := MarshalVendorList(vendorList{ + GVLSpecificationVersion: 2, + VendorListVersion: 2, + Vendors: map[string]*vendor{ + "2": { + ID: 2, + Purposes: []int{}, + }, + }, + }) + specVersion3vendorListData := MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, + VendorListVersion: 2, + Vendors: map[string]*vendor{ + "2": { + ID: 2, + Purposes: []int{1}, + }, + }, + }) + + tcf2AggConfig := tcf2Config{ + HostConfig: config.TCF2{ + Purpose1: config.TCF2Purpose{ + EnforcePurpose: true, + EnforceVendors: true, + }, + }, + } + tcf2AggConfig.HostConfig.PurposeConfigs = map[consentconstants.Purpose]*config.TCF2Purpose{ + consentconstants.Purpose(1): &tcf2AggConfig.HostConfig.Purpose1, + } + + perms := permissionsImpl{ + cfg: &tcf2AggConfig, + hostVendorID: 2, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + }, + fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{ + 2: { + 1: parseVendorListDataV2(t, specVersion2vendorListData), + }, + 3: { + 1: parseVendorListDataV2(t, specVersion3vendorListData), + }, + }), + purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), + gdprSignal: SignalYes, + } + + tests := []struct { + name string + consent string + expectedAllowSync bool + expectedErr bool + }{ + { + name: "consent_tcf_policy_version_3_uses_gvl_spec_version_2", + consent: policyVersion3WithVendor2AndPurpose1Consent, + expectedAllowSync: false, + }, + { + name: "consent_tcf_policy_version_4_uses_gvl_spec_version_3", + consent: policyVersion4WithVendor2AndPurpose1Consent, + expectedAllowSync: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + perms.consent = tt.consent + allowSync, err := perms.HostCookiesAllowed(context.Background()) + assert.Equal(t, tt.expectedAllowSync, allowSync) + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.Nil(t, err) + } + }) + } +} diff --git a/gdpr/signal.go b/gdpr/signal.go index 8a5317a4590..ed7fe1dd8ea 100644 --- a/gdpr/signal.go +++ b/gdpr/signal.go @@ -16,15 +16,24 @@ const ( var gdprSignalError = &errortypes.BadInput{Message: "GDPR signal should be integer 0 or 1"} -// SignalParse returns a parsed GDPR signal or a parse error. -func SignalParse(rawSignal string) (Signal, error) { - if rawSignal == "" { +// StrSignalParse returns a parsed GDPR signal or a parse error. +func StrSignalParse(signal string) (Signal, error) { + if signal == "" { return SignalAmbiguous, nil } - i, err := strconv.Atoi(rawSignal) + i, err := strconv.Atoi(signal) - if err != nil || (i != 0 && i != 1) { + if err != nil { + return SignalAmbiguous, gdprSignalError + } + + return IntSignalParse(i) +} + +// IntSignalParse checks parameter i is not out of bounds and returns a GDPR signal error +func IntSignalParse(i int) (Signal, error) { + if i != 0 && i != 1 { return SignalAmbiguous, gdprSignalError } diff --git a/gdpr/signal_test.go b/gdpr/signal_test.go index 4c7b0aabdfb..3e8d3c4f95f 100644 --- a/gdpr/signal_test.go +++ b/gdpr/signal_test.go @@ -43,18 +43,71 @@ func TestSignalParse(t *testing.T) { wantSignal: SignalAmbiguous, wantError: true, }, + { + description: "Out of bounds signal - raw signal is 5", + rawSignal: "5", + wantSignal: SignalAmbiguous, + wantError: true, + }, } for _, test := range tests { - signal, err := SignalParse(test.rawSignal) + t.Run(test.description, func(t *testing.T) { + signal, err := StrSignalParse(test.rawSignal) + + assert.Equal(t, test.wantSignal, signal, test.description) - assert.Equal(t, test.wantSignal, signal, test.description) + if test.wantError { + assert.NotNil(t, err, test.description) + } else { + assert.Nil(t, err, test.description) + } + }) + } +} + +func TestIntSignalParse(t *testing.T) { + type testOutput struct { + signal Signal + err error + } + testCases := []struct { + desc string + input int + expected testOutput + }{ + { + desc: "input out of bounds, return SgnalAmbituous and gdprSignalError", + input: -1, + expected: testOutput{ + signal: SignalAmbiguous, + err: gdprSignalError, + }, + }, + { + desc: "input in bounds equals signalNo, return signalNo and nil error", + input: 0, + expected: testOutput{ + signal: SignalNo, + err: nil, + }, + }, + { + desc: "input in bounds equals signalYes, return signalYes and nil error", + input: 1, + expected: testOutput{ + signal: SignalYes, + err: nil, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + outSignal, outErr := IntSignalParse(tc.input) - if test.wantError { - assert.NotNil(t, err, test.description) - } else { - assert.Nil(t, err, test.description) - } + assert.Equal(t, tc.expected.signal, outSignal, tc.desc) + assert.Equal(t, tc.expected.err, outErr, tc.desc) + }) } } diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index 864cf00f856..da6bdbae415 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -18,8 +18,8 @@ import ( "golang.org/x/net/context/ctxhttp" ) -type saveVendors func(uint16, api.VendorList) -type VendorListFetcher func(ctx context.Context, id uint16) (vendorlist.VendorList, error) +type saveVendors func(uint16, uint16, api.VendorList) +type VendorListFetcher func(ctx context.Context, specVersion uint16, listVersion uint16) (vendorlist.VendorList, error) // This file provides the vendorlist-fetching function for Prebid Server. // @@ -27,7 +27,7 @@ type VendorListFetcher func(ctx context.Context, id uint16) (vendorlist.VendorLi // // Nothing in this file is exported. Public APIs can be found in gdpr.go -func NewVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16) string) VendorListFetcher { +func NewVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16, uint16) string) VendorListFetcher { cacheSave, cacheLoad := newVendorListCache() preloadContext, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) @@ -35,50 +35,62 @@ func NewVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http preloadCache(preloadContext, client, urlMaker, cacheSave) saveOneRateLimited := newOccasionalSaver(cfg.Timeouts.ActiveTimeout()) - return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + return func(ctx context.Context, specVersion, listVersion uint16) (vendorlist.VendorList, error) { // Attempt To Load From Cache - if list := cacheLoad(vendorListVersion); list != nil { + if list := cacheLoad(specVersion, listVersion); list != nil { return list, nil } // Attempt To Download // - May not add to cache immediately. - saveOneRateLimited(ctx, client, urlMaker(vendorListVersion), cacheSave) + saveOneRateLimited(ctx, client, urlMaker(specVersion, listVersion), cacheSave) // Attempt To Load From Cache Again // - May have been added by the call to saveOneRateLimited. - if list := cacheLoad(vendorListVersion); list != nil { + if list := cacheLoad(specVersion, listVersion); list != nil { return list, nil } // Give Up - return nil, makeVendorListNotFoundError(vendorListVersion) + return nil, makeVendorListNotFoundError(specVersion, listVersion) } } -func makeVendorListNotFoundError(vendorListVersion uint16) error { - return fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", vendorListVersion) +func makeVendorListNotFoundError(specVersion, listVersion uint16) error { + return fmt.Errorf("gdpr vendor list spec version %d list version %d does not exist, or has not been loaded yet. Try again in a few minutes", specVersion, listVersion) } // preloadCache saves all the known versions of the vendor list for future use. -func preloadCache(ctx context.Context, client *http.Client, urlMaker func(uint16) string, saver saveVendors) { - latestVersion := saveOne(ctx, client, urlMaker(0), saver) - - // The GVL for TCF2 has no vendors defined in its first version. It's very unlikely to be used, so don't preload it. - firstVersionToLoad := uint16(2) +func preloadCache(ctx context.Context, client *http.Client, urlMaker func(uint16, uint16) string, saver saveVendors) { + versions := [2]struct { + specVersion uint16 + firstListVersion uint16 + }{ + { + specVersion: 2, + firstListVersion: 2, // The GVL for TCF2 has no vendors defined in its first version. It's very unlikely to be used, so don't preload it. + }, + { + specVersion: 3, + firstListVersion: 1, + }, + } + for _, v := range versions { + latestVersion := saveOne(ctx, client, urlMaker(v.specVersion, 0), saver) - for i := firstVersionToLoad; i < latestVersion; i++ { - saveOne(ctx, client, urlMaker(i), saver) + for i := v.firstListVersion; i < latestVersion; i++ { + saveOne(ctx, client, urlMaker(v.specVersion, i), saver) + } } } // Make a URL which can be used to fetch a given version of the Global Vendor List. If the version is 0, // this will fetch the latest version. -func VendorListURLMaker(vendorListVersion uint16) string { - if vendorListVersion == 0 { - return "https://vendor-list.consensu.org/v2/vendor-list.json" +func VendorListURLMaker(specVersion, listVersion uint16) string { + if listVersion == 0 { + return "https://vendor-list.consensu.org/v" + strconv.Itoa(int(specVersion)) + "/vendor-list.json" } - return "https://vendor-list.consensu.org/v2/archives/vendor-list-v" + strconv.Itoa(int(vendorListVersion)) + ".json" + return "https://vendor-list.consensu.org/v" + strconv.Itoa(int(specVersion)) + "/archives/vendor-list-v" + strconv.Itoa(int(listVersion)) + ".json" } // newOccasionalSaver returns a wrapped version of saveOne() which only activates every few minutes. @@ -133,19 +145,21 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return 0 } - saver(newList.Version(), newList) + saver(newList.SpecVersion(), newList.Version(), newList) return newList.Version() } -func newVendorListCache() (save func(vendorListVersion uint16, list api.VendorList), load func(vendorListVersion uint16) api.VendorList) { +func newVendorListCache() (save func(specVersion, listVersion uint16, list api.VendorList), load func(specVersion, listVersion uint16) api.VendorList) { cache := &sync.Map{} - save = func(vendorListVersion uint16, list api.VendorList) { - cache.Store(vendorListVersion, list) + save = func(specVersion uint16, listVersion uint16, list api.VendorList) { + key := fmt.Sprint(specVersion) + "-" + fmt.Sprint(listVersion) + cache.Store(key, list) } - load = func(vendorListVersion uint16) api.VendorList { - list, ok := cache.Load(vendorListVersion) + load = func(specVersion, listVersion uint16) api.VendorList { + key := fmt.Sprint(specVersion) + "-" + fmt.Sprint(listVersion) + list, ok := cache.Load(key) if ok { return list.(vendorlist.VendorList) } diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 2c5eb4297a3..a1dfb7fefb8 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/prebid/go-gdpr/api" "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/config" ) @@ -20,9 +21,11 @@ func TestFetcherDynamicLoadListExists(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ vendorListLatestVersion: 1, - vendorLists: map[int]string{ - 1: vendorList1, - 2: vendorList2, + vendorLists: map[int]map[int]string{ + 3: { + 1: vendorList1, + 2: vendorList2, + }, }, }))) defer server.Close() @@ -30,7 +33,8 @@ func TestFetcherDynamicLoadListExists(t *testing.T) { test := test{ description: "Dynamic Load - List Exists", setup: testSetup{ - vendorListVersion: 2, + specVersion: 3, + listVersion: 2, }, expected: vendorList2Expected, } @@ -44,8 +48,10 @@ func TestFetcherDynamicLoadListDoesntExist(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ vendorListLatestVersion: 1, - vendorLists: map[int]string{ - 1: vendorList1, + vendorLists: map[int]map[int]string{ + 3: { + 1: vendorList1, + }, }, }))) defer server.Close() @@ -53,10 +59,11 @@ func TestFetcherDynamicLoadListDoesntExist(t *testing.T) { test := test{ description: "No Fallback - Vendor Doesn't Exist", setup: testSetup{ - vendorListVersion: 2, + specVersion: 3, + listVersion: 2, }, expected: testExpected{ - errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + errorMessage: "gdpr vendor list spec version 3 list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", }, } @@ -66,19 +73,24 @@ func TestFetcherDynamicLoadListDoesntExist(t *testing.T) { func TestFetcherThrottling(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ vendorListLatestVersion: 1, - vendorLists: map[int]string{ - 1: MarshalVendorList(vendorList{ - VendorListVersion: 1, - Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1}}}, - }), - 2: MarshalVendorList(vendorList{ - VendorListVersion: 2, - Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2}}}, - }), - 3: MarshalVendorList(vendorList{ - VendorListVersion: 3, - Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}}, - }), + vendorLists: map[int]map[int]string{ + 3: { + 1: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, + VendorListVersion: 1, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1}}}, + }), + 2: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, + VendorListVersion: 2, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2}}}, + }), + 3: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, + VendorListVersion: 3, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}}, + }), + }, }, }))) defer server.Close() @@ -86,26 +98,28 @@ func TestFetcherThrottling(t *testing.T) { fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) // Dynamically Load List 2 Successfully - _, errList1 := fetcher(context.Background(), 2) + _, errList1 := fetcher(context.Background(), 3, 2) assert.NoError(t, errList1) // Fail To Load List 3 Due To Rate Limiting // - The request is rate limited after dynamically list 2. - _, errList2 := fetcher(context.Background(), 3) - assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes") + _, errList2 := fetcher(context.Background(), 3, 3) + assert.EqualError(t, errList2, "gdpr vendor list spec version 3 list version 3 does not exist, or has not been loaded yet. Try again in a few minutes") } func TestMalformedVendorlist(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ vendorListLatestVersion: 1, - vendorLists: map[int]string{ - 1: "malformed", + vendorLists: map[int]map[int]string{ + 3: { + 1: "malformed", + }, }, }))) defer server.Close() fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) - _, err := fetcher(context.Background(), 1) + _, err := fetcher(context.Background(), 3, 1) // Fetching should fail since vendor list could not be unmarshalled. assert.Error(t, err) @@ -115,12 +129,12 @@ func TestServerUrlInvalid(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - invalidURLGenerator := func(uint16) string { return " http://invalid-url-has-leading-whitespace" } + invalidURLGenerator := func(uint16, uint16) string { return " http://invalid-url-has-leading-whitespace" } fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator) - _, err := fetcher(context.Background(), 1) + _, err := fetcher(context.Background(), 3, 1) - assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") + assert.EqualError(t, err, "gdpr vendor list spec version 3 list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") } func TestServerUnavailable(t *testing.T) { @@ -128,43 +142,140 @@ func TestServerUnavailable(t *testing.T) { server.Close() fetcher := NewVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) - _, err := fetcher(context.Background(), 1) + _, err := fetcher(context.Background(), 3, 1) - assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") + assert.EqualError(t, err, "gdpr vendor list spec version 3 list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") } func TestVendorListURLMaker(t *testing.T) { testCases := []struct { - description string - vendorListVersion uint16 - expectedURL string + description string + specVersion uint16 + listVersion uint16 + expectedURL string }{ { - description: "Latest", - vendorListVersion: 0, - expectedURL: "https://vendor-list.consensu.org/v2/vendor-list.json", + description: "Spec version 2 latest list", + specVersion: 2, + listVersion: 0, + expectedURL: "https://vendor-list.consensu.org/v2/vendor-list.json", }, { - description: "Specific", - vendorListVersion: 42, - expectedURL: "https://vendor-list.consensu.org/v2/archives/vendor-list-v42.json", + description: "Spec version 2 specific list", + specVersion: 2, + listVersion: 42, + expectedURL: "https://vendor-list.consensu.org/v2/archives/vendor-list-v42.json", + }, + { + description: "Spec version 3 latest list", + specVersion: 3, + listVersion: 0, + expectedURL: "https://vendor-list.consensu.org/v3/vendor-list.json", + }, + { + description: "Spec version 3 specific list", + specVersion: 3, + listVersion: 42, + expectedURL: "https://vendor-list.consensu.org/v3/archives/vendor-list-v42.json", }, } for _, test := range testCases { - result := VendorListURLMaker(test.vendorListVersion) + result := VendorListURLMaker(test.specVersion, test.listVersion) assert.Equal(t, test.expectedURL, result) } } +type versionInfo struct { + specVersion uint16 + listVersion uint16 +} +type saver []versionInfo + +func (s *saver) saveVendorLists(specVersion uint16, listVersion uint16, gvl api.VendorList) { + vi := versionInfo{ + specVersion: specVersion, + listVersion: listVersion, + } + *s = append(*s, vi) +} + +func TestPreloadCache(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 3, + vendorLists: map[int]map[int]string{ + 1: { + 1: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 1, VendorListVersion: 1, + }), + 2: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 1, VendorListVersion: 2, + }), + 3: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 1, VendorListVersion: 3, + }), + }, + 2: { + 1: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 2, VendorListVersion: 1, + }), + 2: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 2, VendorListVersion: 2, + }), + 3: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 2, VendorListVersion: 3, + }), + }, + 3: { + 1: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, VendorListVersion: 1, + }), + 2: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, VendorListVersion: 2, + }), + 3: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 3, VendorListVersion: 3, + }), + }, + 4: { + 1: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 4, VendorListVersion: 1, + }), + 2: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 4, VendorListVersion: 2, + }), + 3: MarshalVendorList(vendorList{ + GVLSpecificationVersion: 4, VendorListVersion: 3, + }), + }, + }, + }))) + defer server.Close() + + s := make(saver, 0, 5) + preloadCache(context.Background(), server.Client(), testURLMaker(server), s.saveVendorLists) + + expectedLoadedVersions := []versionInfo{ + {specVersion: 2, listVersion: 2}, + {specVersion: 2, listVersion: 3}, + {specVersion: 3, listVersion: 1}, + {specVersion: 3, listVersion: 2}, + {specVersion: 3, listVersion: 3}, + } + + assert.ElementsMatch(t, expectedLoadedVersions, s) +} + var vendorList1 = MarshalVendorList(vendorList{ - VendorListVersion: 1, - Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{2}}}, + GVLSpecificationVersion: 3, + VendorListVersion: 1, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{2}}}, }) var vendorList2 = MarshalVendorList(vendorList{ - VendorListVersion: 2, - Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{2, 3}}}, + GVLSpecificationVersion: 3, + VendorListVersion: 2, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{2, 3}}}, }) var vendorList2Expected = testExpected{ @@ -180,8 +291,9 @@ var vendorListFallbackExpected = testExpected{ } type vendorList struct { - VendorListVersion uint16 `json:"vendorListVersion"` - Vendors map[string]*vendor `json:"vendors"` + GVLSpecificationVersion uint16 `json:"gvlSpecificationVersion"` + VendorListVersion uint16 `json:"vendorListVersion"` + Vendors map[string]*vendor `json:"vendors"` } type vendor struct { @@ -199,7 +311,7 @@ func MarshalVendorList(vendorList vendorList) string { type serverSettings struct { vendorListLatestVersion int - vendorLists map[int]string + vendorLists map[int]map[int]string } // mockServer returns a handler which returns the given response for each global vendor list version. @@ -215,20 +327,33 @@ type serverSettings struct { // Don't ask why... that's just what the official page is doing. See https://vendor-list.consensu.org/v-9999/vendorlist.json func mockServer(settings serverSettings) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, req *http.Request) { - vendorListVersion := req.URL.Query().Get("version") - vendorListVersionInt, err := strconv.Atoi(vendorListVersion) + specVersion := req.URL.Query().Get("specversion") + specVersionInt, err := strconv.Atoi(specVersion) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Request had invalid spec version: " + specVersion)) + return + } + listVersion := req.URL.Query().Get("listversion") + listVersionInt, err := strconv.Atoi(listVersion) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Request had invalid version: " + vendorListVersion)) + w.Write([]byte("Request had invalid version: " + listVersion)) return } - if vendorListVersionInt == 0 { - vendorListVersionInt = settings.vendorListLatestVersion + if listVersionInt == 0 { + listVersionInt = settings.vendorListLatestVersion } - response, ok := settings.vendorLists[vendorListVersionInt] + specVersionVendorLists, ok := settings.vendorLists[specVersionInt] if !ok { w.WriteHeader(http.StatusForbidden) - w.Write([]byte("Version not found: " + vendorListVersion)) + w.Write([]byte("Version not found: spec version " + specVersion + " list version " + listVersion)) + return + } + response, ok := specVersionVendorLists[listVersionInt] + if !ok { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("Version not found: " + listVersion)) return } w.Write([]byte(response)) @@ -242,7 +367,8 @@ type test struct { } type testSetup struct { - vendorListVersion uint16 + specVersion uint16 + listVersion uint16 } type testExpected struct { @@ -255,7 +381,7 @@ type testExpected struct { func runTest(t *testing.T, test test, server *httptest.Server) { config := testConfig() fetcher := NewVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server)) - vendorList, err := fetcher(context.Background(), test.setup.vendorListVersion) + vendorList, err := fetcher(context.Background(), test.setup.specVersion, test.setup.listVersion) if test.expected.errorMessage != "" { assert.EqualError(t, err, test.expected.errorMessage, test.description+":error") @@ -270,10 +396,10 @@ func runTest(t *testing.T, test test, server *httptest.Server) { } } -func testURLMaker(server *httptest.Server) func(uint16) string { +func testURLMaker(server *httptest.Server) func(uint16, uint16) string { url := server.URL - return func(vendorListVersion uint16) string { - return url + "?version=" + strconv.Itoa(int(vendorListVersion)) + return func(specVersion, listVersion uint16) string { + return url + "?specversion=" + strconv.Itoa(int(specVersion)) + "&listversion=" + strconv.Itoa(int(listVersion)) } } diff --git a/go.mod b/go.mod index 14aae8de87b..6731c177ecf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/prebid/prebid-server -go 1.19 +go 1.20 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 @@ -19,7 +19,7 @@ require ( github.com/lib/pq v1.10.4 github.com/mitchellh/copystructure v1.2.0 github.com/pkg/errors v0.9.1 - github.com/prebid/go-gdpr v1.11.0 + github.com/prebid/go-gdpr v1.12.0 github.com/prebid/go-gpp v0.1.1 github.com/prebid/openrtb/v19 v19.0.0 github.com/prometheus/client_golang v1.12.1 @@ -33,14 +33,14 @@ require ( github.com/yudai/gojsondiff v1.0.0 golang.org/x/net v0.7.0 golang.org/x/text v0.7.0 - google.golang.org/grpc v1.46.2 + google.golang.org/grpc v1.53.0 gopkg.in/evanphx/json-patch.v4 v4.12.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -68,8 +68,8 @@ require ( github.com/yudai/pp v2.0.1+incompatible // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/sys v0.5.0 // indirect - google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect - google.golang.org/protobuf v1.28.0 // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 8e64faa69aa..1f76be7af31 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,9 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c h1:eXqCBUHfmjbeDqcuvzjsd+bM6A+bnwo5N9FVbV6m5/s= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c/go.mod h1:omJZNg0Qu76bxJd+ExohVo8uXzNcGOk2bv7vel460xk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -125,7 +126,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -210,7 +210,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -392,8 +392,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prebid/go-gdpr v1.11.0 h1:QbMjscuw3Ul0mDVWeMy5tP0Kii6lmTSSVhV6fm8rY9s= -github.com/prebid/go-gdpr v1.11.0/go.mod h1:mPZAdkRxn+iuSjaUuJAi9+0SppBOdM1PCzv/55UH3pY= +github.com/prebid/go-gdpr v1.12.0 h1:OrjQ7Uc+lCRYaOirQ48jjG/PBMvZsKNAaRTgzxN6iZ0= +github.com/prebid/go-gdpr v1.12.0/go.mod h1:mPZAdkRxn+iuSjaUuJAi9+0SppBOdM1PCzv/55UH3pY= github.com/prebid/go-gpp v0.1.1 h1:uTMJ+eHmKWL9WvDuxFT4LDoOeJW1yOsfWITqi49ZuY0= github.com/prebid/go-gpp v0.1.1/go.mod h1:b0TLoVln+HXFD9L9xeimxIH3FN8WDKPJ42auslxEkow= github.com/prebid/openrtb/v19 v19.0.0 h1:NA7okrg7KcvL5wEg6yI0mAyujpyfkC8XSQr3h5ocN88= @@ -908,8 +908,8 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -940,9 +940,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -957,8 +956,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hooks/hookexecution/enricher.go b/hooks/hookexecution/enricher.go index ef517c5082d..2978c21957d 100644 --- a/hooks/hookexecution/enricher.go +++ b/hooks/hookexecution/enricher.go @@ -6,7 +6,6 @@ import ( "github.com/buger/jsonparser" "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/hooks/hookanalytics" jsonpatch "gopkg.in/evanphx/json-patch.v4" ) @@ -169,7 +168,6 @@ func prepareModulesOutcome(modulesOutcome *ModulesOutcome, groups []GroupOutcome for i, hookOutcome := range group.InvocationResults { if !trace.isVerbose() { group.InvocationResults[i].DebugMessages = nil - group.InvocationResults[i].AnalyticsTags = hookanalytics.Analytics{} } if isDebugEnabled { diff --git a/hooks/hookexecution/execution.go b/hooks/hookexecution/execution.go index 20fa76d8ab6..18c927896b9 100644 --- a/hooks/hookexecution/execution.go +++ b/hooks/hookexecution/execution.go @@ -190,12 +190,10 @@ func handleHookResponse[P any]( ExecutionTime: ExecutionTime{ExecutionTimeMillis: hr.ExecutionTime}, } - switch true { - case hr.Err != nil: + if hr.Err != nil || hr.Result.Reject { handleHookError(hr, &hookOutcome, metricEngine, labels) - case hr.Result.Reject: rejectErr = handleHookReject(ctx, hr, &hookOutcome, metricEngine, labels) - default: + } else { payload = handleHookMutations(payload, hr, &hookOutcome, metricEngine, labels) } diff --git a/hooks/hookexecution/executor.go b/hooks/hookexecution/executor.go index ce516630778..5074d4b9ab9 100644 --- a/hooks/hookexecution/executor.go +++ b/hooks/hookexecution/executor.go @@ -160,7 +160,7 @@ func (e *hookExecutor) ExecuteProcessedAuctionStage(request *openrtb_ext.Request stageName := hooks.StageProcessedAuctionRequest.String() executionCtx := e.newContext(stageName) - payload := hookstage.ProcessedAuctionRequestPayload{BidRequest: request.BidRequest} + payload := hookstage.ProcessedAuctionRequestPayload{RequestWrapper: request} outcome, _, contexts, reject := executeStage(executionCtx, plan, payload, handler, e.metricEngine) outcome.Entity = entityAuctionRequest diff --git a/hooks/hookexecution/mocks_test.go b/hooks/hookexecution/mocks_test.go index e8670ff89e8..ff543c38a07 100644 --- a/hooks/hookexecution/mocks_test.go +++ b/hooks/hookexecution/mocks_test.go @@ -131,7 +131,7 @@ func (e mockTimeoutHook) HandleProcessedAuctionHook(_ context.Context, _ hooksta time.Sleep(20 * time.Millisecond) c := hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{} c.AddMutation(func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) { - payload.BidRequest.User.CustomData = "some-custom-data" + payload.RequestWrapper.User.CustomData = "some-custom-data" return payload, nil }, hookstage.MutationUpdate, "bidRequest", "user.customData") @@ -305,12 +305,20 @@ func (e mockUpdateBidRequestHook) HandleProcessedAuctionHook(_ context.Context, c := hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{} c.AddMutation( func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) { - payload.BidRequest.User.Yob = 2000 + payload.RequestWrapper.User.Yob = 2000 + userExt, err := payload.RequestWrapper.GetUserExt() + if err != nil { + return payload, err + } + newPrebidExt := &openrtb_ext.ExtUserPrebid{ + BuyerUIDs: map[string]string{"some": "id"}, + } + userExt.SetPrebid(newPrebidExt) return payload, nil }, hookstage.MutationUpdate, "bidRequest", "user.yob", ).AddMutation( func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) { - payload.BidRequest.User.Consent = "true" + payload.RequestWrapper.User.Consent = "true" return payload, nil }, hookstage.MutationUpdate, "bidRequest", "user.consent", ) diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json index 1f52d1878d6..591f6e914be 100644 --- a/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json +++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json @@ -47,7 +47,32 @@ "status": "success", "action": "update", "execution_time_millis": 200, - "analytics_tags": {}, + "analytics_tags": { + "activities": [ + { + "name": "device-id", + "status": "success", + "results": [ + { + "status": "success-allow", + "values": { + "foo": "bar" + }, + "appliedto": { + "impids": [ + "impId1" + ], + "request": true + } + } + ] + }, + { + "name": "define-blocks", + "status": "error" + } + ] + }, "message": "" }, { diff --git a/hooks/hookstage/processedauctionrequest.go b/hooks/hookstage/processedauctionrequest.go index 30cb0dab38d..fe06bc6fdbd 100644 --- a/hooks/hookstage/processedauctionrequest.go +++ b/hooks/hookstage/processedauctionrequest.go @@ -3,7 +3,7 @@ package hookstage import ( "context" - "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" ) // ProcessedAuctionRequest hooks are invoked after the request is parsed @@ -23,8 +23,8 @@ type ProcessedAuctionRequest interface { ) (HookResult[ProcessedAuctionRequestPayload], error) } -// ProcessedAuctionRequestPayload consists of the openrtb2.BidRequest object. -// Hooks are allowed to modify openrtb2.BidRequest using mutations. +// ProcessedAuctionRequestPayload consists of the openrtb_ext.RequestWrapper object. +// Hooks are allowed to modify openrtb_ext.RequestWrapper using mutations. type ProcessedAuctionRequestPayload struct { - BidRequest *openrtb2.BidRequest + RequestWrapper *openrtb_ext.RequestWrapper } diff --git a/hooks/repo.go b/hooks/repo.go index 033a3f8f7a0..40276701b34 100644 --- a/hooks/repo.go +++ b/hooks/repo.go @@ -2,6 +2,7 @@ package hooks import ( "fmt" + "github.com/prebid/prebid-server/hooks/hookstage" ) @@ -50,7 +51,7 @@ type hookRepository struct { auctionResponseHooks map[string]hookstage.AuctionResponse } -func (r *hookRepository) GetEntrypointHook(id string) (h hookstage.Entrypoint, ok bool) { +func (r *hookRepository) GetEntrypointHook(id string) (hookstage.Entrypoint, bool) { return getHook(r.entrypointHooks, id) } diff --git a/macros/provider.go b/macros/provider.go new file mode 100644 index 00000000000..0b0fc0de454 --- /dev/null +++ b/macros/provider.go @@ -0,0 +1,145 @@ +package macros + +import ( + "net/url" + "strconv" + "time" + + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + MacroKeyBidID = "PBS-BIDID" + MacroKeyAppBundle = "PBS-APPBUNDLE" + MacroKeyDomain = "PBS-DOMAIN" + MacroKeyPubDomain = "PBS-PUBDOMAIN" + MacroKeyPageURL = "PBS-PAGEURL" + MacroKeyAccountID = "PBS-ACCOUNTID" + MacroKeyLmtTracking = "PBS-LIMITADTRACKING" + MacroKeyConsent = "PBS-GDPRCONSENT" + MacroKeyBidder = "PBS-BIDDER" + MacroKeyIntegration = "PBS-INTEGRATION" + MacroKeyVastCRTID = "PBS-VASTCRTID" + MacroKeyTimestamp = "PBS-TIMESTAMP" + MacroKeyAuctionID = "PBS-AUCTIONID" + MacroKeyChannel = "PBS-CHANNEL" + MacroKeyEventType = "PBS-EVENTTYPE" + MacroKeyVastEvent = "PBS-VASTEVENT" +) + +const ( + customMacroLength = 100 + CustomMacroPrefix = "PBS-MACRO-" +) + +type macroProvider struct { + // macros map stores macros key values + macros map[string]string +} + +// NewBuilder returns the instance of macro buidler +func NewProvider(reqWrapper *openrtb_ext.RequestWrapper) *macroProvider { + macroProvider := ¯oProvider{macros: map[string]string{}} + macroProvider.populateRequestMacros(reqWrapper) + return macroProvider +} + +func (b *macroProvider) populateRequestMacros(reqWrapper *openrtb_ext.RequestWrapper) { + b.macros[MacroKeyTimestamp] = strconv.Itoa(int(time.Now().Unix())) + reqExt, err := reqWrapper.GetRequestExt() + if err == nil && reqExt != nil { + if reqPrebidExt := reqExt.GetPrebid(); reqPrebidExt != nil { + for key, value := range reqPrebidExt.Macros { + customMacroKey := CustomMacroPrefix + key // Adding prefix PBS-MACRO to custom macro keys + b.macros[customMacroKey] = truncate(value, customMacroLength) // limit the custom macro value to 100 chars only + } + + if reqPrebidExt.Integration != "" { + b.macros[MacroKeyIntegration] = reqPrebidExt.Integration + } + + if reqPrebidExt.Channel != nil { + b.macros[MacroKeyChannel] = reqPrebidExt.Channel.Name + } + } + } + b.macros[MacroKeyAuctionID] = reqWrapper.ID + if reqWrapper.App != nil { + if reqWrapper.App.Bundle != "" { + b.macros[MacroKeyAppBundle] = reqWrapper.App.Bundle + } + + if reqWrapper.App.Domain != "" { + b.macros[MacroKeyDomain] = reqWrapper.App.Domain + } + + if reqWrapper.App.Publisher != nil { + if reqWrapper.App.Publisher.Domain != "" { + b.macros[MacroKeyPubDomain] = reqWrapper.App.Publisher.Domain + } + if reqWrapper.App.Publisher.ID != "" { + b.macros[MacroKeyAccountID] = reqWrapper.App.Publisher.ID + } + } + } + + if reqWrapper.Site != nil { + if reqWrapper.Site.Page != "" { + b.macros[MacroKeyPageURL] = reqWrapper.Site.Page + } + + if reqWrapper.Site.Domain != "" { + b.macros[MacroKeyDomain] = reqWrapper.Site.Domain + } + + if reqWrapper.Site.Publisher != nil { + if reqWrapper.Site.Publisher.Domain != "" { + b.macros[MacroKeyPubDomain] = reqWrapper.Site.Publisher.Domain + } + + if reqWrapper.Site.Publisher.ID != "" { + b.macros[MacroKeyAccountID] = reqWrapper.Site.Publisher.ID + } + } + } + + userExt, err := reqWrapper.GetUserExt() + if err == nil && userExt != nil && userExt.GetConsent() != nil { + b.macros[MacroKeyConsent] = *userExt.GetConsent() + } + if reqWrapper.Device != nil && reqWrapper.Device.Lmt != nil { + b.macros[MacroKeyLmtTracking] = strconv.Itoa(int(*reqWrapper.Device.Lmt)) + } + +} + +func (b *macroProvider) GetMacro(key string) string { + return url.QueryEscape(b.macros[key]) +} + +func (b *macroProvider) PopulateBidMacros(bid *entities.PbsOrtbBid, seat string) { + if bid.Bid != nil { + if bid.GeneratedBidID != "" { + b.macros[MacroKeyBidID] = bid.GeneratedBidID + } else { + b.macros[MacroKeyBidID] = bid.Bid.ID + } + } + b.macros[MacroKeyBidder] = seat +} + +func (b *macroProvider) PopulateEventMacros(vastCreativeID, eventType, vastEvent string) { + b.macros[MacroKeyVastCRTID] = vastCreativeID + b.macros[MacroKeyEventType] = eventType + b.macros[MacroKeyVastEvent] = vastEvent +} + +func truncate(text string, width uint) string { + r := []rune(text) + if uint(len(r)) < (width) { + return text + } + trunc := r[:width] + return string(trunc) +} diff --git a/macros/provider_test.go b/macros/provider_test.go new file mode 100644 index 00000000000..b6465a7f2e6 --- /dev/null +++ b/macros/provider_test.go @@ -0,0 +1,464 @@ +package macros + +import ( + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestGetMacro(t *testing.T) { + type args struct { + key string + reqWrapper *openrtb_ext.RequestWrapper + seat string + vastEvent string + eventType string + vastCreativeID string + } + tests := []struct { + name string + args args + want string + }{ + { + name: " Macro present, get PBS-APPBUNDLE key", + args: args{ + key: MacroKeyAppBundle, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + App: &openrtb2.App{ + Bundle: "test", + }, + }, + }, + }, + want: "test", + }, + { + name: " Macro does present, get PBS-APPBUNDLE key", + args: args{ + key: MacroKeyAppBundle, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + }, + }, + }, + want: "", + }, + { + name: "Invalid Macro key", + args: args{ + key: "PBS-NOEXISTENTKEY", + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR3":"a"}}}`), + }, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + macroProvider := NewProvider(tt.args.reqWrapper) + macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, tt.args.seat) + macroProvider.PopulateEventMacros(tt.args.vastCreativeID, tt.args.eventType, tt.args.vastEvent) + got := macroProvider.GetMacro(tt.args.key) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestPopulateRequestMacros(t *testing.T) { + type args struct { + reqWrapper *openrtb_ext.RequestWrapper + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "No request level macros present", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + }, + want: map[string]string{MacroKeyBidID: "", MacroKeyAppBundle: "", MacroKeyDomain: "", MacroKeyPubDomain: "", MacroKeyPageURL: "", MacroKeyAccountID: "", MacroKeyLmtTracking: "", MacroKeyConsent: "", MacroKeyBidder: "", MacroKeyIntegration: "", MacroKeyVastCRTID: "", MacroKeyAuctionID: "", MacroKeyChannel: "", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + { + name: " AUCTIONID, AppBundle, PageURL present key", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + App: &openrtb2.App{ + Bundle: "testBundle", + }, + Site: &openrtb2.Site{ + Page: "testPage", + }, + }, + }, + }, + want: map[string]string{MacroKeyBidID: "", MacroKeyAppBundle: "testBundle", MacroKeyDomain: "", MacroKeyPubDomain: "", MacroKeyPageURL: "testPage", MacroKeyAccountID: "", MacroKeyLmtTracking: "", MacroKeyConsent: "", MacroKeyBidder: "", MacroKeyIntegration: "", MacroKeyVastCRTID: "", MacroKeyAuctionID: "123", MacroKeyChannel: "", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + { + name: " AppDomain, PubDomain, present key", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Domain: "testDomain", + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + Domain: "pubDomain", + }, + }, + }, + }, + }, + want: map[string]string{MacroKeyBidID: "", MacroKeyAppBundle: "", MacroKeyDomain: "testDomain", MacroKeyPubDomain: "pubDomain", MacroKeyPageURL: "", MacroKeyAccountID: "", MacroKeyLmtTracking: "", MacroKeyConsent: "", MacroKeyBidder: "", MacroKeyIntegration: "", MacroKeyVastCRTID: "", MacroKeyAuctionID: "", MacroKeyChannel: "", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + { + name: " Integration, Consent present key", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{Ext: []byte(`{"consent":"1" }`)}, + Ext: []byte(`{"prebid":{"integration":"testIntegration"}}`), + }, + }, + }, + want: map[string]string{MacroKeyBidID: "", MacroKeyAppBundle: "", MacroKeyDomain: "", MacroKeyPubDomain: "", MacroKeyPageURL: "", MacroKeyAccountID: "", MacroKeyLmtTracking: "", MacroKeyConsent: "1", MacroKeyBidder: "", MacroKeyIntegration: "testIntegration", MacroKeyVastCRTID: "", MacroKeyAuctionID: "", MacroKeyChannel: "", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + { + name: " PBS-CHANNEL, LIMITADTRACKING present key", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Lmt: &lmt, + }, + Ext: []byte(`{"prebid":{"channel": {"name":"test1"}}}`), + }, + }, + }, + want: map[string]string{MacroKeyBidID: "", MacroKeyAppBundle: "", MacroKeyDomain: "", MacroKeyPubDomain: "", MacroKeyPageURL: "", MacroKeyAccountID: "", MacroKeyLmtTracking: "10", MacroKeyConsent: "", MacroKeyBidder: "", MacroKeyIntegration: "", MacroKeyVastCRTID: "", MacroKeyAuctionID: "", MacroKeyChannel: "test1", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + { + name: " custom macros present key", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"macros":{"CUSTOMMACR1":"value1"}}}`), + }, + }, + }, + want: map[string]string{"PBS-MACRO-CUSTOMMACR1": "value1", MacroKeyBidID: "", MacroKeyAppBundle: "", MacroKeyDomain: "", MacroKeyPubDomain: "", MacroKeyPageURL: "", MacroKeyAccountID: "", MacroKeyLmtTracking: "", MacroKeyConsent: "", MacroKeyBidder: "", MacroKeyIntegration: "", MacroKeyVastCRTID: "", MacroKeyAuctionID: "", MacroKeyChannel: "", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + { + name: " All request macros present key", + args: args{ + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + Site: &openrtb2.Site{ + Domain: "testdomain", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + Page: "pageurltest", + }, + App: &openrtb2.App{ + Domain: "testdomain", + Bundle: "testbundle", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + }, + Device: &openrtb2.Device{ + Lmt: &lmt, + }, + User: &openrtb2.User{Ext: []byte(`{"consent":"1" }`)}, + Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR1":"value1"}}}`), + }, + }, + }, + want: map[string]string{"PBS-MACRO-CUSTOMMACR1": "value1", MacroKeyBidID: "", MacroKeyAppBundle: "testbundle", MacroKeyDomain: "testdomain", MacroKeyPubDomain: "publishertestdomain", MacroKeyPageURL: "pageurltest", MacroKeyAccountID: "testpublisherID", MacroKeyLmtTracking: "10", MacroKeyConsent: "1", MacroKeyBidder: "", MacroKeyIntegration: "", MacroKeyVastCRTID: "", MacroKeyAuctionID: "123", MacroKeyChannel: "test1", MacroKeyEventType: "", MacroKeyVastEvent: ""}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := ¯oProvider{ + macros: map[string]string{}, + } + b.populateRequestMacros(tt.args.reqWrapper) + output := map[string]string{} + for key := range tt.want { + output[key] = b.GetMacro(key) + } + assert.Equal(t, tt.want, output, tt.name) + }) + } +} + +func TestPopulateBidMacros(t *testing.T) { + + type args struct { + bid *entities.PbsOrtbBid + seat string + } + tests := []struct { + name string + args args + wantBidID string + wantSeat string + }{ + { + name: "Bid ID set, no generatedbid id, no seat", + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, + }, + wantBidID: "bid123", + wantSeat: "", + }, + { + name: "Bid ID set, no generatedbid id, seat set", + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, + seat: "testSeat", + }, + wantBidID: "bid123", + wantSeat: "testSeat", + }, + { + name: "Bid ID set, generatedbid id set, no seat", + args: args{ + bid: &entities.PbsOrtbBid{ + GeneratedBidID: "generatedbid123", + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, + }, + wantBidID: "generatedbid123", + wantSeat: "", + }, + { + name: "Bid ID set, generatedbid id set, seat set", + args: args{ + bid: &entities.PbsOrtbBid{ + GeneratedBidID: "generatedbid123", + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, + seat: "testseat", + }, + wantBidID: "generatedbid123", + wantSeat: "testseat", + }, + { + name: "Bid ID not set, generatedbid id set, seat set", + args: args{ + seat: "test-seat", + bid: &entities.PbsOrtbBid{ + GeneratedBidID: "generatedbid123", + Bid: &openrtb2.Bid{}, + }, + }, + wantBidID: "generatedbid123", + wantSeat: "test-seat", + }, + { + name: "Bid ID not set, generatedbid id not set, seat set", + args: args{ + seat: "test-seat", + bid: &entities.PbsOrtbBid{}, + }, + wantBidID: "", + wantSeat: "test-seat", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := ¯oProvider{ + macros: map[string]string{}, + } + b.PopulateBidMacros(tt.args.bid, tt.args.seat) + assert.Equal(t, tt.wantBidID, b.GetMacro(MacroKeyBidID), tt.name) + assert.Equal(t, tt.wantSeat, b.GetMacro(MacroKeyBidder), tt.name) + }) + } +} +func TestPopulateEventMacros(t *testing.T) { + + type args struct { + vastCreativeID string + eventType string + vastEvent string + } + tests := []struct { + name string + args args + wantVastCreativeID string + wantEventType string + wantVastEvent string + }{ + { + name: "creativeId not set, eventType not set, vastEvent not set", + args: args{}, + wantVastCreativeID: "", + wantEventType: "", + wantVastEvent: "", + }, + { + name: "creativeId set, eventType not set, vastEvent not set", + args: args{ + vastCreativeID: "123", + }, + wantVastCreativeID: "123", + wantEventType: "", + wantVastEvent: "", + }, + { + name: "creativeId not set, eventType set, vastEvent not set", + args: args{ + eventType: "win", + }, + wantVastCreativeID: "", + wantEventType: "win", + wantVastEvent: "", + }, + { + name: "creativeId not set, eventType not set, vastEvent set", + args: args{ + vastEvent: "firstQuartile", + }, + wantVastCreativeID: "", + wantEventType: "", + wantVastEvent: "firstQuartile", + }, + { + name: "creativeId not set, eventType set, vastEvent set", + args: args{ + vastEvent: "firstQuartile", + eventType: "win", + }, + wantVastCreativeID: "", + wantEventType: "win", + wantVastEvent: "firstQuartile", + }, + { + name: "creativeId set, eventType not set, vastEvent set", + args: args{ + vastEvent: "firstQuartile", + vastCreativeID: "123", + }, + wantVastCreativeID: "123", + wantEventType: "", + wantVastEvent: "firstQuartile", + }, + { + name: "creativeId set, eventType set, vastEvent not set", + args: args{ + eventType: "win", + vastCreativeID: "123", + }, + wantVastCreativeID: "123", + wantEventType: "win", + wantVastEvent: "", + }, + { + name: "creativeId set, eventType set, vastEvent set", + args: args{ + vastEvent: "firstQuartile", + eventType: "win", + vastCreativeID: "123", + }, + wantVastCreativeID: "123", + wantEventType: "win", + wantVastEvent: "firstQuartile", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := ¯oProvider{ + macros: map[string]string{}, + } + b.PopulateEventMacros(tt.args.vastCreativeID, tt.args.eventType, tt.args.vastEvent) + assert.Equal(t, tt.wantVastCreativeID, b.GetMacro(MacroKeyVastCRTID), tt.name) + assert.Equal(t, tt.wantVastEvent, b.GetMacro(MacroKeyVastEvent), tt.name) + assert.Equal(t, tt.wantEventType, b.GetMacro(MacroKeyEventType), tt.name) + }) + } +} + +func TestTruncate(t *testing.T) { + type args struct { + text string + width uint + } + tests := []struct { + name string + args args + want string + }{ + { + name: "text is empty", + args: args{ + text: "", + width: customMacroLength, + }, + want: "", + }, + { + name: "width less than 100 chars", + args: args{ + text: "abcdef", + width: customMacroLength, + }, + want: "abcdef", + }, + { + name: "width exactly 100 chars", + args: args{ + text: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv", + width: customMacroLength, + }, + want: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv", + }, + { + name: "width greater than 100 chars", + args: args{ + text: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", + width: customMacroLength, + }, + want: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := truncate(tt.args.text, tt.args.width) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} diff --git a/macros/replacer.go b/macros/replacer.go new file mode 100644 index 00000000000..e1a13d27636 --- /dev/null +++ b/macros/replacer.go @@ -0,0 +1,7 @@ +package macros + +type Replacer interface { + // Replace the macros and returns replaced string + // if any error the error will be returned + Replace(url string, macroProvider *macroProvider) (string, error) +} diff --git a/macros/string_index_based_replacer.go b/macros/string_index_based_replacer.go new file mode 100644 index 00000000000..32748c1ed76 --- /dev/null +++ b/macros/string_index_based_replacer.go @@ -0,0 +1,104 @@ +package macros + +import ( + "strings" + "sync" +) + +const ( + delimiter = "##" +) + +type stringIndexBasedReplacer struct { + templates map[string]urlMetaTemplate + sync.RWMutex +} + +type urlMetaTemplate struct { + startingIndices []int + endingIndices []int +} + +// NewStringIndexBasedReplacer will return instance of string index based macro replacer +func NewStringIndexBasedReplacer() Replacer { + return &stringIndexBasedReplacer{ + templates: make(map[string]urlMetaTemplate), + } +} + +// constructTemplate func finds index bounds of all macros in an input string where macro format is ##data##. +// constructTemplate func returns two arrays with start indexes and end indexes for all macros found in the input string. +// Start index of the macro points to the index of the delimiter(##) start. +// End index of the macro points to the end index of the delimiter. +// For the valid input string number of start and end indexes should be equal, and they should not intersect. +// This approach shows better performance results compare to standard GoLang string replacer. +func constructTemplate(url string) urlMetaTemplate { + currentIndex := 0 + tmplt := urlMetaTemplate{ + startingIndices: []int{}, + endingIndices: []int{}, + } + delimiterLen := len(delimiter) + for { + currentIndex = currentIndex + strings.Index(url[currentIndex:], delimiter) + if currentIndex == -1 { + break + } + startIndex := currentIndex + delimiterLen + endingIndex := strings.Index(url[startIndex:], delimiter) + if endingIndex == -1 { + break + } + endingIndex = endingIndex + startIndex - 1 + tmplt.startingIndices = append(tmplt.startingIndices, startIndex) + tmplt.endingIndices = append(tmplt.endingIndices, endingIndex) + currentIndex = endingIndex + delimiterLen + 1 + if currentIndex >= len(url)-1 { + break + } + } + return tmplt +} + +// Replace function replaces macros in a given string with the data from macroProvider and returns modified input string. +// If a given string was previously processed this function fetches its metadata from the cache. +// If input string is not found in cache then template metadata will be created. +// Iterates over start and end indexes of the template arrays and extracts macro name from the input string. +// Gets the value of the extracted macro from the macroProvider. Replaces macro with corresponding value. +func (s *stringIndexBasedReplacer) Replace(url string, macroProvider *macroProvider) (string, error) { + tmplt := s.getTemplate(url) + + var result strings.Builder + currentIndex := 0 + delimLen := len(delimiter) + for i, index := range tmplt.startingIndices { + macro := url[index : tmplt.endingIndices[i]+1] + // copy prev part + result.WriteString(url[currentIndex : index-delimLen]) + value := macroProvider.GetMacro(macro) + if value != "" { + result.WriteString(value) + } + currentIndex = index + len(macro) + delimLen + } + result.WriteString(url[currentIndex:]) + return result.String(), nil +} + +func (s *stringIndexBasedReplacer) getTemplate(url string) urlMetaTemplate { + var ( + template urlMetaTemplate + ok bool + ) + s.RLock() + template, ok = s.templates[url] + s.RUnlock() + + if !ok { + s.Lock() + template = constructTemplate(url) + s.templates[url] = template + s.Unlock() + } + return template +} diff --git a/macros/string_index_based_replacer_test.go b/macros/string_index_based_replacer_test.go new file mode 100644 index 00000000000..97379a6d965 --- /dev/null +++ b/macros/string_index_based_replacer_test.go @@ -0,0 +1,146 @@ +package macros + +import ( + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/exchange/entities" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestStringIndexBasedReplace(t *testing.T) { + + type args struct { + url string + getMacroProvider func() *macroProvider + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "success", + args: args{ + url: "http://tracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-DOMAIN##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o7=##PBS-LIMITADTRACKING##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1##¯o10=##PBS-BIDDER##¯o11=##PBS-INTEGRATION##¯o12=##PBS-VASTCRTID##¯o15=##PBS-AUCTIONID##¯o16=##PBS-CHANNEL##¯o17=##PBS-EVENTTYPE##¯o18=##PBS-VASTEVENT##", + getMacroProvider: func() *macroProvider { + macroProvider := NewProvider(req) + macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") + macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") + return macroProvider + }, + }, + want: "http://tracker.com?macro1=bidId123¯o2=testbundle¯o3=testdomain¯o4=publishertestdomain¯o5=pageurltest¯o6=testpublisherID¯o7=10¯o8=yes¯o9=value1¯o10=test¯o11=¯o12=123¯o15=123¯o16=test1¯o17=vast¯o18=firstQuartile", + wantErr: false, + }, + { + name: "url does not have macro", + args: args{ + url: "http://tracker.com", + getMacroProvider: func() *macroProvider { + macroProvider := NewProvider(req) + macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") + macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") + return macroProvider + }, + }, + want: "http://tracker.com", + wantErr: false, + }, + { + name: "macro not found", + args: args{ + url: "http://tracker.com?macro1=##PBS-test1##", + getMacroProvider: func() *macroProvider { + macroProvider := NewProvider(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}) + macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") + macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") + return macroProvider + }, + }, + want: "http://tracker.com?macro1=", + wantErr: false, + }, + { + name: "tracker url is empty", + args: args{ + url: "", + getMacroProvider: func() *macroProvider { + macroProvider := NewProvider(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}) + macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") + macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") + return macroProvider + }, + }, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + replacer := NewStringIndexBasedReplacer() + got, err := replacer.Replace(tt.args.url, tt.args.getMacroProvider()) + if tt.wantErr { + assert.Error(t, err, tt.name) + } else { + assert.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got, tt.name) + } + }) + } +} + +var lmt int8 = 10 +var benchmarkURL = []string{ + "http://tracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##", + "http://google.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##", + "http://pubmatic.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##", + "http://testbidder.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##", + "http://dummybidder.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##", +} + +var req *openrtb_ext.RequestWrapper = &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + Site: &openrtb2.Site{ + Domain: "testdomain", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + Page: "pageurltest", + }, + App: &openrtb2.App{ + Domain: "testdomain", + Bundle: "testbundle", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + }, + Device: &openrtb2.Device{ + Lmt: &lmt, + }, + User: &openrtb2.User{Ext: []byte(`{"consent":"yes" }`)}, + Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR1":"value1","CUSTOMMACR2":"value2","CUSTOMMACR3":"value3"}}}`), + }, +} + +var bid *openrtb2.Bid = &openrtb2.Bid{ID: "bidId123", CID: "campaign_1", CrID: "creative_1"} + +func BenchmarkStringIndexBasedReplacer(b *testing.B) { + replacer := NewStringIndexBasedReplacer() + for n := 0; n < b.N; n++ { + for _, url := range benchmarkURL { + macroProvider := NewProvider(req) + macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") + macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") + _, err := replacer.Replace(url, macroProvider) + if err != nil { + b.Errorf("Fail to replace macro in tracker") + } + } + } +} diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go index cc9f4da0428..bf173253e56 100644 --- a/metrics/config/metrics.go +++ b/metrics/config/metrics.go @@ -80,6 +80,12 @@ func (me *MultiMetricsEngine) RecordConnectionAccept(success bool) { } } +func (me *MultiMetricsEngine) RecordTMaxTimeout() { + for _, thisME := range *me { + thisME.RecordTMaxTimeout() + } +} + func (me *MultiMetricsEngine) RecordConnectionClose(success bool) { for _, thisME := range *me { thisME.RecordConnectionClose(success) @@ -149,6 +155,12 @@ func (me *MultiMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Durat } } +func (me *MultiMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) { + for _, thisME := range *me { + thisME.RecordBidderServerResponseTime(bidderServerResponseTime) + } +} + // RecordAdapterBidReceived across all engines func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { for _, thisME := range *me { @@ -170,6 +182,13 @@ func (me *MultiMetricsEngine) RecordAdapterTime(labels metrics.AdapterLabels, le } } +// RecordOverheadTime across all engines +func (me *MultiMetricsEngine) RecordOverheadTime(overhead metrics.OverheadType, length time.Duration) { + for _, thisME := range *me { + thisME.RecordOverheadTime(overhead, length) + } +} + // RecordCookieSync across all engines func (me *MultiMetricsEngine) RecordCookieSync(status metrics.CookieSyncStatus) { for _, thisME := range *me { @@ -378,6 +397,10 @@ func (me *NilMetricsEngine) RecordRequest(labels metrics.Labels) { func (me *NilMetricsEngine) RecordConnectionAccept(success bool) { } +// RecordTMaxTimeout as a noop +func (me *NilMetricsEngine) RecordTMaxTimeout() { +} + // RecordConnectionClose as a noop func (me *NilMetricsEngine) RecordConnectionClose(success bool) { } @@ -418,6 +441,10 @@ func (me *NilMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { func (me *NilMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { } +// RecordBidderServerResponseTime as a noop +func (me *NilMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) { +} + // RecordAdapterBidReceived as a noop func (me *NilMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { } @@ -430,6 +457,10 @@ func (me *NilMetricsEngine) RecordAdapterPrice(labels metrics.AdapterLabels, cpm func (me *NilMetricsEngine) RecordAdapterTime(labels metrics.AdapterLabels, length time.Duration) { } +// RecordOverheadTime as a noop +func (me *NilMetricsEngine) RecordOverheadTime(overhead metrics.OverheadType, length time.Duration) { +} + // RecordCookieSync as a noop func (me *NilMetricsEngine) RecordCookieSync(status metrics.CookieSyncStatus) { } diff --git a/metrics/config/metrics_test.go b/metrics/config/metrics_test.go index 80aa2550d3f..f77dcf005d8 100644 --- a/metrics/config/metrics_test.go +++ b/metrics/config/metrics_test.go @@ -34,7 +34,7 @@ func TestGoMetricsEngine(t *testing.T) { testEngine := NewMetricsEngine(&cfg, adapterList, syncerKeys, modulesStages) _, ok := testEngine.MetricsEngine.(*metrics.Metrics) if !ok { - t.Error("Expected a legacy Metrics as MetricsEngine, but didn't get it") + t.Error("Expected a Metrics as MetricsEngine, but didn't get it") } } @@ -148,7 +148,6 @@ func TestMultiMetricsEngine(t *testing.T) { //Make the metrics engine, instantiated here with goEngine, fill its RequestStatuses[RequestType][metrics.RequestStatusXX] with the new boolean values added to metrics.Labels VerifyMetrics(t, "RequestStatuses.OpenRTB2.OK", goEngine.RequestStatuses[metrics.ReqTypeORTB2Web][metrics.RequestStatusOK].Count(), 5) - VerifyMetrics(t, "RequestStatuses.Legacy.OK", goEngine.RequestStatuses[metrics.ReqTypeLegacy][metrics.RequestStatusOK].Count(), 0) VerifyMetrics(t, "RequestStatuses.AMP.OK", goEngine.RequestStatuses[metrics.ReqTypeAMP][metrics.RequestStatusOK].Count(), 0) VerifyMetrics(t, "RequestStatuses.AMP.BlacklistedAcctOrApp", goEngine.RequestStatuses[metrics.ReqTypeAMP][metrics.RequestStatusBlacklisted].Count(), 1) VerifyMetrics(t, "RequestStatuses.Video.OK", goEngine.RequestStatuses[metrics.ReqTypeVideo][metrics.RequestStatusOK].Count(), 0) diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go index 8563fcb113c..382dc64c090 100644 --- a/metrics/go_metrics.go +++ b/metrics/go_metrics.go @@ -11,10 +11,10 @@ import ( metrics "github.com/rcrowley/go-metrics" ) -// Metrics is the legacy Metrics object (go-metrics) expanded to also satisfy the MetricsEngine interface type Metrics struct { MetricsRegistry metrics.Registry ConnectionCounter metrics.Counter + TMaxTimeoutCounter metrics.Counter ConnectionAcceptErrorMeter metrics.Meter ConnectionCloseErrorMeter metrics.Meter ImpMeter metrics.Meter @@ -32,10 +32,10 @@ type Metrics struct { AccountCacheMeter map[CacheResult]metrics.Meter DNSLookupTimer metrics.Timer TLSHandshakeTimer metrics.Timer + BidderServerResponseTimer metrics.Timer StoredResponsesMeter metrics.Meter - // Metrics for OpenRTB requests specifically. So we can track what % of RequestsMeter are OpenRTB - // and know when legacy requests have been abandoned. + // Metrics for OpenRTB requests specifically RequestStatuses map[RequestType]map[RequestStatus]metrics.Meter AmpNoCookieMeter metrics.Meter CookieSyncMeter metrics.Meter @@ -79,6 +79,8 @@ type Metrics struct { // Module metrics ModuleMetrics map[string]map[string]*ModuleMetrics + + OverheadTimer map[OverheadType]metrics.Timer } // AdapterMetrics houses the metrics for a particular adapter @@ -155,7 +157,7 @@ type ModuleMetrics struct { // testing routines to ensure that no metrics are written anywhere. // // This will be useful when removing endpoints, we can just run will the blank metrics function -// rather than loading legacy metrics that never get filled. +// rather than loading metrics that never get filled. // This will also eventually let us configure metrics, such as setting a limited set of metrics // for a production instance, and then expanding again when we need more debugging. func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disabledMetrics config.DisabledMetrics, moduleStageNames map[string][]string) *Metrics { @@ -218,6 +220,9 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa exchanges: exchanges, modules: getModuleNames(moduleStageNames), + + OverheadTimer: makeBlankOverheadTimerMetrics(), + BidderServerResponseTimer: blankTimer, } for _, a := range exchanges { @@ -284,6 +289,7 @@ func getModuleNames(moduleStageNames map[string][]string) []string { func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disableAccountMetrics config.DisabledMetrics, syncerKeys []string, moduleStageNames map[string][]string) *Metrics { newMetrics := NewBlankMetrics(registry, exchanges, disableAccountMetrics, moduleStageNames) newMetrics.ConnectionCounter = metrics.GetOrRegisterCounter("active_connections", registry) + newMetrics.TMaxTimeoutCounter = metrics.GetOrRegisterCounter("tmax_timeout", registry) newMetrics.ConnectionAcceptErrorMeter = metrics.GetOrRegisterMeter("connection_accept_errors", registry) newMetrics.ConnectionCloseErrorMeter = metrics.GetOrRegisterMeter("connection_close_errors", registry) newMetrics.ImpMeter = metrics.GetOrRegisterMeter("imps_requested", registry) @@ -302,6 +308,8 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry) newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry) newMetrics.StoredResponsesMeter = metrics.GetOrRegisterMeter("stored_responses", registry) + newMetrics.OverheadTimer = makeOverheadTimerMetrics(registry) + newMetrics.BidderServerResponseTimer = metrics.GetOrRegisterTimer("bidder_server_response_time_seconds", registry) for _, dt := range StoredDataTypes() { for _, ft := range StoredDataFetchTypes() { @@ -379,6 +387,15 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d return newMetrics } +func makeBlankOverheadTimerMetrics() map[OverheadType]metrics.Timer { + m := make(map[OverheadType]metrics.Timer) + overheads := OverheadTypes() + for idx := range overheads { + m[overheads[idx]] = &metrics.NilTimer{} + } + return m +} + // Part of setting up blank metrics, the adapter metrics. func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMetrics { blankMeter := &metrics.NilMeter{} @@ -445,6 +462,15 @@ func makeBlankMarkupDeliveryMetrics() *MarkupDeliveryMetrics { } } +func makeOverheadTimerMetrics(registry metrics.Registry) map[OverheadType]metrics.Timer { + m := make(map[OverheadType]metrics.Timer) + overheads := OverheadTypes() + for idx := range overheads { + m[overheads[idx]] = metrics.GetOrRegisterTimer("request_over_head_time."+overheads[idx].String(), registry) + } + return m +} + func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string, exchange string, am *AdapterMetrics) { am.NoCookieMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.no_cookie_requests", adapterOrAccount, exchange), registry) am.NoBidMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.requests.nobid", adapterOrAccount, exchange), registry) @@ -689,6 +715,10 @@ func (me *Metrics) RecordConnectionAccept(success bool) { } } +func (m *Metrics) RecordTMaxTimeout() { + m.TMaxTimeoutCounter.Inc(1) +} + func (me *Metrics) RecordConnectionClose(success bool) { if success { me.ConnectionCounter.Dec(1) @@ -790,6 +820,10 @@ func (me *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { me.TLSHandshakeTimer.Update(tlsHandshakeTime) } +func (me *Metrics) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) { + me.BidderServerResponseTimer.Update(bidderServerResponseTime) +} + // RecordAdapterBidReceived implements a part of the MetricsEngine interface. // This tracks how many bids from each Bidder use `adm` vs. `nurl. func (me *Metrics) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { @@ -847,6 +881,11 @@ func (me *Metrics) RecordAdapterTime(labels AdapterLabels, length time.Duration) } } +// RecordOverheadTime implements a part of the MetricsEngine interface. Records the adapter overhead time +func (me *Metrics) RecordOverheadTime(overhead OverheadType, length time.Duration) { + me.OverheadTimer[overhead].Update(length) +} + // RecordCookieSync implements a part of the MetricsEngine interface. Records a cookie sync request func (me *Metrics) RecordCookieSync(status CookieSyncStatus) { me.CookieSyncMeter.Mark(1) diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go index f5b860db8fa..515691fe503 100644 --- a/metrics/go_metrics_test.go +++ b/metrics/go_metrics_test.go @@ -80,6 +80,12 @@ func TestNewMetrics(t *testing.T) { ensureContains(t, registry, "ads_cert_requests.ok", m.AdsCertRequestsSuccess) ensureContains(t, registry, "ads_cert_requests.failed", m.AdsCertRequestsFailure) + ensureContains(t, registry, "request_over_head_time.pre-bidder", m.OverheadTimer[PreBidder]) + ensureContains(t, registry, "request_over_head_time.make-auction-response", m.OverheadTimer[MakeAuctionResponse]) + ensureContains(t, registry, "request_over_head_time.make-bidder-requests", m.OverheadTimer[MakeBidderRequests]) + ensureContains(t, registry, "bidder_server_response_time_seconds", m.BidderServerResponseTimer) + ensureContains(t, registry, "tmax_timeout", m.TMaxTimeoutCounter) + for module, stages := range moduleStageNames { for _, stage := range stages { ensureContainsModuleMetrics(t, registry, fmt.Sprintf("modules.module.%s.stage.%s", module, stage), m.ModuleMetrics[module][stage]) @@ -416,6 +422,36 @@ func TestRecordTLSHandshakeTime(t *testing.T) { } } +func TestRecordBidderServerResponseTime(t *testing.T) { + testCases := []struct { + name string + time time.Duration + expectedCount int64 + expectedSum int64 + }{ + { + name: "record-bidder-server-response-time-1", + time: time.Duration(500), + expectedCount: 1, + expectedSum: 500, + }, + { + name: "record-bidder-server-response-time-2", + time: time.Duration(500), + expectedCount: 2, + expectedSum: 1000, + }, + } + for _, test := range testCases { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true}, nil, nil) + + m.RecordBidderServerResponseTime(test.time) + + assert.Equal(t, test.time.Nanoseconds(), m.BidderServerResponseTimer.Sum(), test.name) + } +} + func TestRecordAdapterConnections(t *testing.T) { var fakeBidder openrtb_ext.BidderName = "fooAdvertising" @@ -1193,6 +1229,55 @@ func TestRecordAccountUpgradeStatusMetrics(t *testing.T) { } } +func TestRecordOverheadTime(t *testing.T) { + testCases := []struct { + name string + time time.Duration + overheadType OverheadType + expectedCount int64 + expectedSum int64 + }{ + { + name: "record-pre-bidder-overhead-time-1", + time: time.Duration(500), + overheadType: PreBidder, + expectedCount: 1, + expectedSum: 500, + }, + { + name: "record-pre-bidder-overhead-time-2", + time: time.Duration(500), + overheadType: PreBidder, + expectedCount: 2, + expectedSum: 1000, + }, + { + name: "record-auction-response-overhead-time", + time: time.Duration(500), + overheadType: MakeAuctionResponse, + expectedCount: 1, + expectedSum: 500, + }, + { + name: "record-make-bidder-requests-overhead-time", + time: time.Duration(500), + overheadType: MakeBidderRequests, + expectedCount: 1, + expectedSum: 500, + }, + } + registry := metrics.NewRegistry() + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + m := NewMetrics(registry, []openrtb_ext.BidderName{}, config.DisabledMetrics{}, nil, nil) + m.RecordOverheadTime(test.overheadType, test.time) + overheadMetrics := m.OverheadTimer[test.overheadType] + assert.Equal(t, test.expectedCount, overheadMetrics.Count()) + assert.Equal(t, test.expectedSum, overheadMetrics.Sum()) + }) + } +} + func ensureContainsBidTypeMetrics(t *testing.T, registry metrics.Registry, prefix string, mdm map[openrtb_ext.BidType]*MarkupDeliveryMetrics) { ensureContains(t, registry, prefix+".banner.adm_bids_received", mdm[openrtb_ext.BidTypeBanner].AdmMeter) ensureContains(t, registry, prefix+".banner.nurl_bids_received", mdm[openrtb_ext.BidTypeBanner].NurlMeter) diff --git a/metrics/metrics.go b/metrics/metrics.go index c1a65b4445f..c30f81675a0 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -26,6 +26,26 @@ type AdapterLabels struct { AdapterErrors map[AdapterError]struct{} } +// OverheadType: overhead type enumeration +type OverheadType string + +const ( + // PreBidder - measures the time needed to execute the adapter's MakeRequests() implementation, build Prebid headers and apply GZip compression if needed + PreBidder OverheadType = "pre-bidder" + // MakeAuctionResponse - measures the amount of time spent doing all the MakeBids() calls as well as preparing PBS's response + MakeAuctionResponse OverheadType = "make-auction-response" + // MakeBidderRequests - measures the time needed to fetch a stored request (if needed), parse, unmarshal, and validate the OpenRTB request, interpret its privacy policies, and split it into multiple requests sanitized for each bidder + MakeBidderRequests OverheadType = "make-bidder-requests" +) + +func (t OverheadType) String() string { + return string(t) +} + +func OverheadTypes() []OverheadType { + return []OverheadType{PreBidder, MakeAuctionResponse, MakeBidderRequests} +} + // ImpLabels defines metric labels describing the impression type. type ImpLabels struct { BannerImps bool @@ -157,24 +177,14 @@ func DemandTypes() []DemandSource { // The request types (endpoints) const ( - ReqTypeLegacy RequestType = "legacy" ReqTypeORTB2Web RequestType = "openrtb2-web" ReqTypeORTB2App RequestType = "openrtb2-app" ReqTypeAMP RequestType = "amp" ReqTypeVideo RequestType = "video" ) -// The media types described in the "imp" json objects -const ( - ImpTypeBanner ImpMediaType = "banner" - ImpTypeVideo ImpMediaType = "video" - ImpTypeAudio ImpMediaType = "audio" - ImpTypeNative ImpMediaType = "native" -) - func RequestTypes() []RequestType { return []RequestType{ - ReqTypeLegacy, ReqTypeORTB2Web, ReqTypeORTB2App, ReqTypeAMP, @@ -182,6 +192,14 @@ func RequestTypes() []RequestType { } } +// The media types described in the "imp" json objects +const ( + ImpTypeBanner ImpMediaType = "banner" + ImpTypeVideo ImpMediaType = "video" + ImpTypeAudio ImpMediaType = "audio" + ImpTypeNative ImpMediaType = "native" +) + func ImpTypes() []ImpMediaType { return []ImpMediaType{ ImpTypeBanner, @@ -403,17 +421,18 @@ func SyncerSetUidStatuses() []SyncerSetUidStatus { // is generally not useful. type MetricsEngine interface { RecordConnectionAccept(success bool) + RecordTMaxTimeout() RecordConnectionClose(success bool) RecordRequest(labels Labels) // ignores adapter. only statusOk and statusErr fom status RecordImps(labels ImpLabels) // RecordImps across openRTB2 engines that support the 'Native' Imp Type RecordRequestTime(labels Labels, length time.Duration) // ignores adapter. only statusOk and statusErr fom status + RecordOverheadTime(overHead OverheadType, length time.Duration) RecordAdapterRequest(labels AdapterLabels) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) RecordDNSTime(dnsLookupTime time.Duration) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) + RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) RecordAdapterPanic(labels AdapterLabels) - // This records whether or not a bid of a particular type uses `adm` or `nurl`. - // Since the legacy endpoints don't have a bid type, it can only count bids from OpenRTB and AMP. RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) RecordAdapterPrice(labels AdapterLabels, cpm float64) RecordAdapterTime(labels AdapterLabels, length time.Duration) diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go index cc73682ecb4..a518b98d404 100644 --- a/metrics/metrics_mock.go +++ b/metrics/metrics_mock.go @@ -22,6 +22,11 @@ func (me *MetricsEngineMock) RecordConnectionAccept(success bool) { me.Called(success) } +// RecordTMaxTimeout mock +func (me *MetricsEngineMock) RecordTMaxTimeout() { + me.Called() +} + // RecordConnectionClose mock func (me *MetricsEngineMock) RecordConnectionClose(success bool) { me.Called(success) @@ -71,6 +76,11 @@ func (me *MetricsEngineMock) RecordTLSHandshakeTime(tlsHandshakeTime time.Durati me.Called(tlsHandshakeTime) } +// RecordBidderServerResponseTime mock +func (me *MetricsEngineMock) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) { + me.Called(bidderServerResponseTime) +} + // RecordAdapterBidReceived mock func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { me.Called(labels, bidType, hasAdm) @@ -86,6 +96,11 @@ func (me *MetricsEngineMock) RecordAdapterTime(labels AdapterLabels, length time me.Called(labels, length) } +// RecordOverheadTime mock +func (me *MetricsEngineMock) RecordOverheadTime(overhead OverheadType, length time.Duration) { + me.Called(overhead, length) +} + // RecordCookieSync mock func (me *MetricsEngineMock) RecordCookieSync(status CookieSyncStatus) { me.Called(status) diff --git a/metrics/prometheus/preload.go b/metrics/prometheus/preload.go index 61ae568d93a..dae7c14dc5b 100644 --- a/metrics/prometheus/preload.go +++ b/metrics/prometheus/preload.go @@ -16,6 +16,7 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st connectionErrorValues = []string{connectionAcceptError, connectionCloseError} cookieValues = cookieTypesAsString() cookieSyncStatusValues = cookieSyncStatusesAsString() + overheadTypes = overheadTypesAsString() requestTypeValues = requestTypesAsString() requestStatusValues = requestStatusesAsString() storedDataFetchTypeValues = storedDataFetchTypesAsString() @@ -187,6 +188,10 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st adapterLabel: adapterValues, }) + preloadLabelValuesForHistogram(m.overheadTimer, map[string][]string{ + overheadTypeLabel: overheadTypes, + }) + preloadLabelValuesForCounter(m.syncerRequests, map[string][]string{ syncerLabel: syncerKeys, statusLabel: syncerRequestStatusValues, diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go index 0d3eaa8234e..c57ba2214f6 100644 --- a/metrics/prometheus/prometheus.go +++ b/metrics/prometheus/prometheus.go @@ -18,13 +18,13 @@ type Metrics struct { Gatherer *prometheus.Registry // General Metrics + tmaxTimeout prometheus.Counter connectionsClosed prometheus.Counter connectionsError *prometheus.CounterVec connectionsOpened prometheus.Counter cookieSync *prometheus.CounterVec setUid *prometheus.CounterVec impressions *prometheus.CounterVec - impressionsLegacy prometheus.Counter prebidCacheWriteTimer *prometheus.HistogramVec requests *prometheus.CounterVec debugRequests prometheus.Counter @@ -56,6 +56,7 @@ type Metrics struct { storedResponsesErrors *prometheus.CounterVec adsCertRequests *prometheus.CounterVec adsCertSignTimer prometheus.Histogram + bidderServerResponseTimer prometheus.Histogram // Adapter Metrics adapterBids *prometheus.CounterVec @@ -63,6 +64,7 @@ type Metrics struct { adapterPanics *prometheus.CounterVec adapterPrices *prometheus.HistogramVec adapterRequests *prometheus.CounterVec + overheadTimer *prometheus.HistogramVec adapterRequestsTimer *prometheus.HistogramVec adapterReusedConnections *prometheus.CounterVec adapterCreatedConnections *prometheus.CounterVec @@ -130,6 +132,7 @@ const ( isVideoLabel = "video" markupDeliveryLabel = "delivery" optOutLabel = "opt_out" + overheadTypeLabel = "overhead_type" privacyBlockedLabel = "privacy_blocked" requestStatusLabel = "request_status" requestTypeLabel = "request_type" @@ -176,6 +179,7 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet cacheWriteTimeBuckets := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1} priceBuckets := []float64{250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000} queuedRequestTimeBuckets := []float64{0, 1, 5, 30, 60, 120, 180, 240, 300} + overheadTimeBuckets := []float64{0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1} metrics := Metrics{} reg := prometheus.NewRegistry() @@ -194,6 +198,10 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet "connections_opened", "Count of successful connections opened to Prebid Server.") + metrics.tmaxTimeout = newCounterWithoutLabels(cfg, reg, + "tmax_timeout", + "Count of requests rejected due to Tmax timeout exceed.") + metrics.cookieSync = newCounter(cfg, reg, "cookie_sync_requests", "Count of cookie sync requests to Prebid Server.", @@ -209,10 +217,6 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet "Count of requested impressions to Prebid Server labeled by type.", []string{isBannerLabel, isVideoLabel, isAudioLabel, isNativeLabel}) - metrics.impressionsLegacy = newCounterWithoutLabels(cfg, reg, - "impressions_requests_legacy", - "Count of requested impressions to Prebid Server using the legacy endpoint.") - metrics.prebidCacheWriteTimer = newHistogramVec(cfg, reg, "prebidcache_write_time_seconds", "Seconds to write to Prebid Cache labeled by success or failure. Failure timing is limited by Prebid Server enforced timeouts.", @@ -430,12 +434,23 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet "Count that tracks number of bids removed from bid response that had a invalid bidAdm (warn)", []string{adapterLabel, successLabel}) + metrics.overheadTimer = newHistogramVec(cfg, reg, + "overhead_time_seconds", + "Seconds to prepare adapter request or resolve adapter response", + []string{overheadTypeLabel}, + overheadTimeBuckets) + metrics.adapterRequestsTimer = newHistogramVec(cfg, reg, "adapter_request_time_seconds", "Seconds to resolve each successful request labeled by adapter.", []string{adapterLabel}, standardTimeBuckets) + metrics.bidderServerResponseTimer = newHistogram(cfg, reg, + "bidder_server_response_time_seconds", + "Duration needed to send HTTP request and receive response back from bidder server.", + standardTimeBuckets) + metrics.syncerRequests = newCounter(cfg, reg, "syncer_requests", "Count of cookie sync requests where a syncer is a candidate to be synced labeled by syncer key and status.", @@ -675,6 +690,10 @@ func (m *Metrics) RecordConnectionAccept(success bool) { } } +func (m *Metrics) RecordTMaxTimeout() { + m.tmaxTimeout.Inc() +} + func (m *Metrics) RecordConnectionClose(success bool) { if success { m.connectionsClosed.Inc() @@ -889,6 +908,10 @@ func (m *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { m.tlsHandhakeTimer.Observe(tlsHandshakeTime.Seconds()) } +func (m *Metrics) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) { + m.bidderServerResponseTimer.Observe(bidderServerResponseTime.Seconds()) +} + func (m *Metrics) RecordAdapterPanic(labels metrics.AdapterLabels) { m.adapterPanics.With(prometheus.Labels{ adapterLabel: string(labels.Adapter), @@ -913,6 +936,12 @@ func (m *Metrics) RecordAdapterPrice(labels metrics.AdapterLabels, cpm float64) }).Observe(cpm) } +func (m *Metrics) RecordOverheadTime(overhead metrics.OverheadType, duration time.Duration) { + m.overheadTimer.With(prometheus.Labels{ + overheadTypeLabel: overhead.String(), + }).Observe(duration.Seconds()) +} + func (m *Metrics) RecordAdapterTime(labels metrics.AdapterLabels, length time.Duration) { if len(labels.AdapterErrors) == 0 { m.adapterRequestsTimer.With(prometheus.Labels{ diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go index 4cb3cd7f00d..32f7848ccff 100644 --- a/metrics/prometheus/prometheus_test.go +++ b/metrics/prometheus/prometheus_test.go @@ -55,7 +55,6 @@ func TestMetricCountGatekeeping(t *testing.T) { // Calculate Per-Adapter Cardinality adapterCount := len(openrtb_ext.CoreBidderNames()) perAdapterCardinalityCount := adapterCardinalityCount / adapterCount - // Verify General Cardinality // - This assertion provides a warning for newly added high-cardinality non-adapter specific metrics. The hardcoded limit // is an arbitrary soft ceiling. Thought should be given as to the value of the new metrics if you find yourself @@ -520,6 +519,52 @@ func TestRequestTimeMetric(t *testing.T) { } } +func TestRecordOverheadTimeMetric(t *testing.T) { + testCases := []struct { + description string + overheadType metrics.OverheadType + timeInMs float64 + expectedCount uint64 + expectedSum float64 + }{ + { + description: "record-pre-bidder-overhead-time-1", + overheadType: metrics.PreBidder, + timeInMs: 500, + expectedCount: 1, + expectedSum: 0.5, + }, + { + description: "record-pre-bidder-overhead-time-2", + overheadType: metrics.PreBidder, + timeInMs: 400, + expectedCount: 2, + expectedSum: 0.9, + }, + { + description: "record-auction-response-overhead-time", + overheadType: metrics.MakeAuctionResponse, + timeInMs: 500, + expectedCount: 1, + expectedSum: 0.5, + }, + { + description: "record-make-bidder-requests-overhead-time", + overheadType: metrics.MakeBidderRequests, + timeInMs: 500, + expectedCount: 1, + expectedSum: 0.5, + }, + } + + metric := createMetricsForTesting() + for _, test := range testCases { + metric.RecordOverheadTime(test.overheadType, time.Duration(test.timeInMs)*time.Millisecond) + resultingHistogram := getHistogramFromHistogramVec(metric.overheadTimer, overheadTypeLabel, test.overheadType.String()) + assertHistogram(t, test.description, resultingHistogram, test.expectedCount, test.expectedSum) + } +} + func TestRecordStoredDataFetchTime(t *testing.T) { tests := []struct { description string @@ -1430,6 +1475,39 @@ func TestRecordTLSHandshakeTime(t *testing.T) { } } +func TestRecordBidderServerResponseTime(t *testing.T) { + testCases := []struct { + description string + timeInMs float64 + expectedCount uint64 + expectedSum float64 + }{ + { + description: "record-bidder-server-response-time-1", + timeInMs: 500, + expectedCount: 1, + expectedSum: 0.5, + }, + { + description: "record-bidder-server-response-time-2", + timeInMs: 400, + expectedCount: 1, + expectedSum: 0.4, + }, + } + for _, test := range testCases { + pm := createMetricsForTesting() + pm.RecordBidderServerResponseTime(time.Duration(test.timeInMs) * time.Millisecond) + + m := dto.Metric{} + pm.bidderServerResponseTimer.Write(&m) + histogram := *m.GetHistogram() + + assert.Equal(t, test.expectedCount, histogram.GetSampleCount()) + assert.Equal(t, test.expectedSum, histogram.GetSampleSum()) + } +} + func TestRecordAdapterConnections(t *testing.T) { type testIn struct { diff --git a/metrics/prometheus/type_conversion.go b/metrics/prometheus/type_conversion.go index d99f2f6f39b..07e4e89c43d 100644 --- a/metrics/prometheus/type_conversion.go +++ b/metrics/prometheus/type_conversion.go @@ -77,6 +77,15 @@ func syncerRequestStatusesAsString() []string { return valuesAsString } +func overheadTypesAsString() []string { + overheadTypes := metrics.OverheadTypes() + overheadTypesAsString := make([]string, len(overheadTypes)) + for i, ot := range overheadTypes { + overheadTypesAsString[i] = ot.String() + } + return overheadTypesAsString +} + func syncerSetStatusesAsString() []string { values := metrics.SyncerSetUidStatuses() valuesAsString := make([]string, len(values)) diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index b242258e99e..85cfed5cbe7 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -25,6 +25,15 @@ type ExtBidPrebid struct { Events *ExtBidPrebidEvents `json:"events,omitempty"` BidId string `json:"bidid,omitempty"` Passthrough json.RawMessage `json:"passthrough,omitempty"` + Floors *ExtBidPrebidFloors `json:"floors,omitempty"` +} + +// ExtBidPrebidFloors defines the contract for bidresponse.seatbid.bid[i].ext.prebid.floors +type ExtBidPrebidFloors struct { + FloorRule string `json:"floorRule,omitempty"` + FloorRuleValue float64 `json:"floorRuleValue,omitempty"` + FloorValue float64 `json:"floorValue,omitempty"` + FloorCurrency string `json:"floorCurrency,omitempty"` } // ExtBidPrebidCache defines the contract for bidresponse.seatbid.bid[i].ext.prebid.cache @@ -41,6 +50,7 @@ type ExtBidPrebidCacheBids struct { // ExtBidPrebidMeta defines the contract for bidresponse.seatbid.bid[i].ext.prebid.meta type ExtBidPrebidMeta struct { + AdapterCode string `json:"adaptercode,omitempty"` AdvertiserDomains []string `json:"advertiserDomains,omitempty"` AdvertiserID int `json:"advertiserId,omitempty"` AdvertiserName string `json:"advertiserName,omitempty"` @@ -48,14 +58,16 @@ type ExtBidPrebidMeta struct { AgencyName string `json:"agencyName,omitempty"` BrandID int `json:"brandId,omitempty"` BrandName string `json:"brandName,omitempty"` - DemandSource string `json:"demandSource,omitempty"` DChain json.RawMessage `json:"dchain,omitempty"` + DemandSource string `json:"demandSource,omitempty"` MediaType string `json:"mediaType,omitempty"` NetworkID int `json:"networkId,omitempty"` NetworkName string `json:"networkName,omitempty"` PrimaryCategoryID string `json:"primaryCatId,omitempty"` + RendererName string `json:"rendererName,omitempty"` + RendererVersion string `json:"rendererVersion,omitempty"` + RendererData json.RawMessage `json:"rendererData,omitempty"` SecondaryCategoryIDs []string `json:"secondaryCatIds,omitempty"` - AdapterCode string `json:"adaptercode,omitempty"` } // ExtBidPrebidVideo defines the contract for bidresponse.seatbid.bid[i].ext.prebid.video diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 4614b05fe68..251e895051e 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -15,6 +15,230 @@ import ( // BidderName refers to a core bidder id or an alias id. type BidderName string +var aliasBidderToParent map[BidderName]BidderName = map[BidderName]BidderName{} + +var coreBidderNames []BidderName = []BidderName{ + Bidder33Across, + BidderAax, + BidderAceex, + BidderAcuityAds, + BidderAdf, + BidderAdform, + BidderAdgeneration, + BidderAdhese, + BidderAdkernel, + BidderAdkernelAdn, + BidderAdman, + BidderAdmixer, + BidderAdnuntius, + BidderAdOcean, + BidderAdoppler, + BidderAdot, + BidderAdpone, + BidderAdprime, + BidderAdquery, + BidderAdrino, + BidderAdsinteractive, + BidderAdsyield, + BidderAdtarget, + BidderAdtrgtme, + BidderAdtelligent, + BidderAdvangelists, + BidderAdView, + BidderAdxcg, + BidderAdyoulike, + BidderAidem, + BidderAJA, + BidderAlgorix, + BidderAMX, + BidderApacdex, + BidderApplogy, + BidderAppnexus, + BidderAppush, + BidderAudienceNetwork, + BidderAutomatad, + BidderAvocet, + BidderAxis, + BidderAxonix, + BidderBeachfront, + BidderBeintoo, + BidderBematterfull, + BidderBetween, + BidderBeyondMedia, + BidderBidmachine, + BidderBidmyadz, + BidderBidsCube, + BidderBidstack, + BidderBizzclick, + BidderBliink, + BidderBlue, + BidderBluesea, + BidderBmtm, + BidderBoldwin, + BidderBrave, + BidderCadentApertureMX, + BidderCcx, + BidderCoinzilla, + BidderColossus, + BidderCompass, + BidderConnectAd, + BidderConsumable, + BidderConversant, + BidderCopper6, + BidderCpmstar, + BidderCriteo, + BidderCWire, + BidderDatablocks, + BidderDecenterAds, + BidderDeepintent, + BidderDefinemedia, + BidderDianomi, + BidderDmx, + BidderEmtv, + BidderEmxDigital, + BidderEngageBDR, + BidderEPlanning, + BidderEpom, + BidderEpsilon, + BidderEVolution, + BidderEvtech, + BidderFlipp, + BidderFreewheelSSP, + BidderFreewheelSSPOld, + BidderFRVRAdNetwork, + BidderGamma, + BidderGamoshi, + BidderGlobalsun, + BidderGothamads, + BidderGrid, + BidderGumGum, + BidderHuaweiAds, + BidderIionads, + BidderImds, + BidderImpactify, + BidderImprovedigital, + BidderInfyTV, + BidderInMobi, + BidderInteractiveoffers, + BidderInvibes, + BidderIQZone, + BidderIx, + BidderJANet, + BidderJixie, + BidderKargo, + BidderKayzen, + BidderKidoz, + BidderKiviads, + BidderLmKiviads, + BidderKrushmedia, + BidderKubient, + BidderLiftoff, + BidderLimelightDigital, + BidderLockerDome, + BidderLogan, + BidderLogicad, + BidderLunaMedia, + BidderMabidder, + BidderMadvertise, + BidderMarsmedia, + BidderMediafuse, + BidderMedianet, + BidderMgid, + BidderMgidX, + BidderMobfoxpb, + BidderMobileFuse, + BidderMotorik, + BidderNanoInteractive, + BidderNextMillennium, + BidderNinthDecimal, + BidderNoBid, + BidderOneTag, + BidderOpenWeb, + BidderOpenx, + BidderOperaads, + BidderOrbidder, + BidderOutbrain, + BidderOwnAdx, + BidderPangle, + BidderPGAM, + BidderPubmatic, + BidderPubnative, + BidderPulsepoint, + BidderPWBid, + BidderQuantumdex, + BidderRevcontent, + BidderRhythmone, + BidderRichaudience, + BidderRise, + BidderRTBHouse, + BidderRubicon, + BidderSeedingAlliance, + BidderSaLunaMedia, + BidderScreencore, + BidderSharethrough, + BidderSilverMob, + BidderSilverPush, + BidderSmaato, + BidderSmartAdserver, + BidderSmartHub, + BidderSmartRTB, + BidderSmartyAds, + BidderSmileWanted, + BidderSonobi, + BidderSovrn, + BidderSspBC, + BidderStreamkey, + BidderStroeerCore, + BidderSuntContent, + BidderSynacormedia, + BidderTaboola, + BidderTappx, + BidderTelaria, + BidderTpmn, + BidderTrafficGate, + BidderTriplelift, + BidderTripleliftNative, + BidderTrustX, + BidderUcfunnel, + BidderUndertone, + BidderUnicorn, + BidderUnruly, + BidderValueImpression, + BidderVideoByte, + BidderVideoHeroes, + BidderVidoomy, + BidderViewdeos, + BidderVisibleMeasures, + BidderVisx, + BidderVox, + BidderVrtcal, + BidderXeworks, + BidderXtrmqb, + BidderYahooAds, + BidderYahooAdvertising, + BidderYahooSSP, + BidderYeahmobi, + BidderYieldlab, + BidderYieldmo, + BidderYieldone, + BidderZeroClickFraud, + BidderZetaGlobalSsp, +} + +func GetAliasBidderToParent() map[BidderName]BidderName { + return aliasBidderToParent +} + +func SetAliasBidderName(aliasBidderName string, parentBidderName BidderName) error { + if IsBidderNameReserved(aliasBidderName) { + return fmt.Errorf("alias %s is a reserved bidder name and cannot be used", aliasBidderName) + } + aliasBidder := BidderName(aliasBidderName) + coreBidderNames = append(coreBidderNames, aliasBidder) + aliasBidderToParent[aliasBidder] = parentBidderName + return nil +} + func (name BidderName) MarshalJSON() ([]byte, error) { return []byte(name), nil } @@ -104,8 +328,10 @@ const ( BidderAdot BidderName = "adot" BidderAdpone BidderName = "adpone" BidderAdprime BidderName = "adprime" + BidderAdquery BidderName = "adquery" BidderAdrino BidderName = "adrino" BidderAdsinteractive BidderName = "adsinteractive" + BidderAdsyield BidderName = "adsyield" BidderAdtarget BidderName = "adtarget" BidderAdtrgtme BidderName = "adtrgtme" BidderAdtelligent BidderName = "adtelligent" @@ -113,6 +339,7 @@ const ( BidderAdView BidderName = "adview" BidderAdxcg BidderName = "adxcg" BidderAdyoulike BidderName = "adyoulike" + BidderAidem BidderName = "aidem" BidderAJA BidderName = "aja" BidderAlgorix BidderName = "algorix" BidderAMX BidderName = "amx" @@ -123,9 +350,11 @@ const ( BidderAudienceNetwork BidderName = "audienceNetwork" BidderAutomatad BidderName = "automatad" BidderAvocet BidderName = "avocet" + BidderAxis BidderName = "axis" BidderAxonix BidderName = "axonix" BidderBeachfront BidderName = "beachfront" BidderBeintoo BidderName = "beintoo" + BidderBematterfull BidderName = "bematterfull" BidderBetween BidderName = "between" BidderBeyondMedia BidderName = "beyondmedia" BidderBidmachine BidderName = "bidmachine" @@ -135,10 +364,11 @@ const ( BidderBizzclick BidderName = "bizzclick" BidderBliink BidderName = "bliink" BidderBlue BidderName = "blue" + BidderBluesea BidderName = "bluesea" BidderBmtm BidderName = "bmtm" BidderBoldwin BidderName = "boldwin" BidderBrave BidderName = "brave" - BidderBrightroll BidderName = "brightroll" + BidderCadentApertureMX BidderName = "cadent_aperture_mx" BidderCcx BidderName = "ccx" BidderCoinzilla BidderName = "coinzilla" BidderColossus BidderName = "colossus" @@ -146,6 +376,7 @@ const ( BidderConnectAd BidderName = "connectad" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" + BidderCopper6 BidderName = "copper6" BidderCpmstar BidderName = "cpmstar" BidderCriteo BidderName = "criteo" BidderCWire BidderName = "cwire" @@ -155,19 +386,23 @@ const ( BidderDefinemedia BidderName = "definemedia" BidderDianomi BidderName = "dianomi" BidderDmx BidderName = "dmx" + BidderEmtv BidderName = "emtv" BidderEmxDigital BidderName = "emx_digital" BidderEngageBDR BidderName = "engagebdr" BidderEPlanning BidderName = "eplanning" + BidderEpsilon BidderName = "epsilon" BidderEpom BidderName = "epom" BidderEVolution BidderName = "e_volution" BidderEvtech BidderName = "evtech" + BidderFlipp BidderName = "flipp" BidderFreewheelSSP BidderName = "freewheelssp" BidderFreewheelSSPOld BidderName = "freewheel-ssp" + BidderFRVRAdNetwork BidderName = "frvradn" BidderGamma BidderName = "gamma" BidderGamoshi BidderName = "gamoshi" BidderGlobalsun BidderName = "globalsun" + BidderGothamads BidderName = "gothamads" BidderGrid BidderName = "grid" - BidderGroupm BidderName = "groupm" BidderGumGum BidderName = "gumgum" BidderHuaweiAds BidderName = "huaweiads" BidderIionads BidderName = "iionads" @@ -186,8 +421,10 @@ const ( BidderKayzen BidderName = "kayzen" BidderKidoz BidderName = "kidoz" BidderKiviads BidderName = "kiviads" + BidderLmKiviads BidderName = "lm_kiviads" BidderKrushmedia BidderName = "krushmedia" BidderKubient BidderName = "kubient" + BidderLiftoff BidderName = "liftoff" BidderLimelightDigital BidderName = "limelightDigital" BidderLockerDome BidderName = "lockerdome" BidderLogan BidderName = "logan" @@ -199,8 +436,10 @@ const ( BidderMediafuse BidderName = "mediafuse" BidderMedianet BidderName = "medianet" BidderMgid BidderName = "mgid" + BidderMgidX BidderName = "mgidX" BidderMobfoxpb BidderName = "mobfoxpb" BidderMobileFuse BidderName = "mobilefuse" + BidderMotorik BidderName = "motorik" BidderNanoInteractive BidderName = "nanointeractive" BidderNextMillennium BidderName = "nextmillennium" BidderNinthDecimal BidderName = "ninthdecimal" @@ -211,6 +450,7 @@ const ( BidderOperaads BidderName = "operaads" BidderOrbidder BidderName = "orbidder" BidderOutbrain BidderName = "outbrain" + BidderOwnAdx BidderName = "ownadx" BidderPangle BidderName = "pangle" BidderPGAM BidderName = "pgam" BidderPubmatic BidderName = "pubmatic" @@ -221,12 +461,15 @@ const ( BidderRevcontent BidderName = "revcontent" BidderRhythmone BidderName = "rhythmone" BidderRichaudience BidderName = "richaudience" + BidderRise BidderName = "rise" BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSeedingAlliance BidderName = "seedingAlliance" BidderSaLunaMedia BidderName = "sa_lunamedia" + BidderScreencore BidderName = "screencore" BidderSharethrough BidderName = "sharethrough" BidderSilverMob BidderName = "silvermob" + BidderSilverPush BidderName = "silverpush" BidderSmaato BidderName = "smaato" BidderSmartAdserver BidderName = "smartadserver" BidderSmartHub BidderName = "smarthub" @@ -243,6 +486,7 @@ const ( BidderTaboola BidderName = "taboola" BidderTappx BidderName = "tappx" BidderTelaria BidderName = "telaria" + BidderTpmn BidderName = "tpmn" BidderTrafficGate BidderName = "trafficgate" BidderTriplelift BidderName = "triplelift" BidderTripleliftNative BidderName = "triplelift_native" @@ -252,206 +496,30 @@ const ( BidderUnicorn BidderName = "unicorn" BidderUnruly BidderName = "unruly" BidderValueImpression BidderName = "valueimpression" - BidderVerizonMedia BidderName = "verizonmedia" BidderVideoByte BidderName = "videobyte" BidderVideoHeroes BidderName = "videoheroes" BidderVidoomy BidderName = "vidoomy" BidderViewdeos BidderName = "viewdeos" BidderVisibleMeasures BidderName = "visiblemeasures" BidderVisx BidderName = "visx" + BidderVox BidderName = "vox" BidderVrtcal BidderName = "vrtcal" + BidderXeworks BidderName = "xeworks" + BidderXtrmqb BidderName = "xtrmqb" + BidderYahooAds BidderName = "yahooAds" + BidderYahooAdvertising BidderName = "yahooAdvertising" BidderYahooSSP BidderName = "yahoossp" BidderYeahmobi BidderName = "yeahmobi" BidderYieldlab BidderName = "yieldlab" BidderYieldmo BidderName = "yieldmo" BidderYieldone BidderName = "yieldone" BidderZeroClickFraud BidderName = "zeroclickfraud" + BidderZetaGlobalSsp BidderName = "zeta_global_ssp" ) // CoreBidderNames returns a slice of all core bidders. func CoreBidderNames() []BidderName { - return []BidderName{ - Bidder33Across, - BidderAax, - BidderAceex, - BidderAcuityAds, - BidderAdf, - BidderAdform, - BidderAdgeneration, - BidderAdhese, - BidderAdkernel, - BidderAdkernelAdn, - BidderAdman, - BidderAdmixer, - BidderAdnuntius, - BidderAdOcean, - BidderAdoppler, - BidderAdot, - BidderAdpone, - BidderAdprime, - BidderAdrino, - BidderAdsinteractive, - BidderAdtarget, - BidderAdtrgtme, - BidderAdtelligent, - BidderAdvangelists, - BidderAdView, - BidderAdxcg, - BidderAdyoulike, - BidderAJA, - BidderAlgorix, - BidderAMX, - BidderApacdex, - BidderApplogy, - BidderAppnexus, - BidderAppush, - BidderAudienceNetwork, - BidderAutomatad, - BidderAvocet, - BidderAxonix, - BidderBeachfront, - BidderBeintoo, - BidderBetween, - BidderBeyondMedia, - BidderBidmachine, - BidderBidmyadz, - BidderBidsCube, - BidderBidstack, - BidderBizzclick, - BidderBliink, - BidderBlue, - BidderBmtm, - BidderBoldwin, - BidderBrave, - BidderBrightroll, - BidderCcx, - BidderCoinzilla, - BidderColossus, - BidderCompass, - BidderConnectAd, - BidderConsumable, - BidderConversant, - BidderCpmstar, - BidderCriteo, - BidderCWire, - BidderDatablocks, - BidderDecenterAds, - BidderDeepintent, - BidderDefinemedia, - BidderDianomi, - BidderDmx, - BidderEmxDigital, - BidderEngageBDR, - BidderEPlanning, - BidderEpom, - BidderEVolution, - BidderEvtech, - BidderFreewheelSSP, - BidderFreewheelSSPOld, - BidderGamma, - BidderGamoshi, - BidderGlobalsun, - BidderGrid, - BidderGroupm, - BidderGumGum, - BidderHuaweiAds, - BidderIionads, - BidderImds, - BidderImpactify, - BidderImprovedigital, - BidderInfyTV, - BidderInMobi, - BidderInteractiveoffers, - BidderInvibes, - BidderIQZone, - BidderIx, - BidderJANet, - BidderJixie, - BidderKargo, - BidderKayzen, - BidderKidoz, - BidderKiviads, - BidderKrushmedia, - BidderKubient, - BidderLimelightDigital, - BidderLockerDome, - BidderLogan, - BidderLogicad, - BidderLunaMedia, - BidderMabidder, - BidderMadvertise, - BidderMarsmedia, - BidderMediafuse, - BidderMedianet, - BidderMgid, - BidderMobfoxpb, - BidderMobileFuse, - BidderNanoInteractive, - BidderNextMillennium, - BidderNinthDecimal, - BidderNoBid, - BidderOneTag, - BidderOpenWeb, - BidderOpenx, - BidderOperaads, - BidderOrbidder, - BidderOutbrain, - BidderPangle, - BidderPGAM, - BidderPubmatic, - BidderPubnative, - BidderPulsepoint, - BidderPWBid, - BidderQuantumdex, - BidderRevcontent, - BidderRhythmone, - BidderRichaudience, - BidderRTBHouse, - BidderRubicon, - BidderSeedingAlliance, - BidderSaLunaMedia, - BidderSharethrough, - BidderSilverMob, - BidderSmaato, - BidderSmartAdserver, - BidderSmartHub, - BidderSmartRTB, - BidderSmartyAds, - BidderSmileWanted, - BidderSonobi, - BidderSovrn, - BidderSspBC, - BidderStreamkey, - BidderStroeerCore, - BidderSuntContent, - BidderSynacormedia, - BidderTaboola, - BidderTappx, - BidderTelaria, - BidderTrafficGate, - BidderTriplelift, - BidderTripleliftNative, - BidderTrustX, - BidderUcfunnel, - BidderUndertone, - BidderUnicorn, - BidderUnruly, - BidderValueImpression, - BidderVerizonMedia, - BidderVideoByte, - BidderVideoHeroes, - BidderVidoomy, - BidderViewdeos, - BidderVisibleMeasures, - BidderVisx, - BidderVrtcal, - BidderYahooSSP, - BidderYeahmobi, - BidderYieldlab, - BidderYieldmo, - BidderYieldone, - BidderZeroClickFraud, - } + return coreBidderNames } // BuildBidderMap builds a map of string to BidderName, to remain compatbile with the @@ -490,11 +558,11 @@ var bidderNameLookup = func() map[string]BidderName { lookup[bidderNameLower] = name } return lookup -}() +} func NormalizeBidderName(name string) (BidderName, bool) { nameLower := strings.ToLower(name) - bidderName, exists := bidderNameLookup[nameLower] + bidderName, exists := bidderNameLookup()[nameLower] return bidderName, exists } @@ -507,10 +575,42 @@ type BidderParamValidator interface { Schema(name BidderName) string } +type bidderParamsFileSystem interface { + readDir(name string) ([]os.DirEntry, error) + readFile(name string) ([]byte, error) + newReferenceLoader(source string) gojsonschema.JSONLoader + newSchema(l gojsonschema.JSONLoader) (*gojsonschema.Schema, error) + abs(path string) (string, error) +} + +type standardBidderParamsFileSystem struct{} + +func (standardBidderParamsFileSystem) readDir(name string) ([]os.DirEntry, error) { + return os.ReadDir(name) +} + +func (standardBidderParamsFileSystem) readFile(name string) ([]byte, error) { + return os.ReadFile(name) +} + +func (standardBidderParamsFileSystem) newReferenceLoader(source string) gojsonschema.JSONLoader { + return gojsonschema.NewReferenceLoader(source) +} + +func (standardBidderParamsFileSystem) newSchema(l gojsonschema.JSONLoader) (*gojsonschema.Schema, error) { + return gojsonschema.NewSchema(l) +} + +func (standardBidderParamsFileSystem) abs(path string) (string, error) { + return filepath.Abs(path) +} + +var paramsValidator bidderParamsFileSystem = standardBidderParamsFileSystem{} + // NewBidderParamsValidator makes a BidderParamValidator, assuming all the necessary files exist in the filesystem. // This will error if, for example, a Bidder gets added but no JSON schema is written for them. func NewBidderParamsValidator(schemaDirectory string) (BidderParamValidator, error) { - fileInfos, err := os.ReadDir(schemaDirectory) + fileInfos, err := paramsValidator.readDir(schemaDirectory) if err != nil { return nil, fmt.Errorf("Failed to read JSON schemas from directory %s. %v", schemaDirectory, err) } @@ -524,17 +624,17 @@ func NewBidderParamsValidator(schemaDirectory string) (BidderParamValidator, err if _, ok := bidderMap[bidderName]; !ok { return nil, fmt.Errorf("File %s/%s does not match a valid BidderName.", schemaDirectory, fileInfo.Name()) } - toOpen, err := filepath.Abs(filepath.Join(schemaDirectory, fileInfo.Name())) + toOpen, err := paramsValidator.abs(filepath.Join(schemaDirectory, fileInfo.Name())) if err != nil { return nil, fmt.Errorf("Failed to get an absolute representation of the path: %s, %v", toOpen, err) } - schemaLoader := gojsonschema.NewReferenceLoader("file:///" + filepath.ToSlash(toOpen)) - loadedSchema, err := gojsonschema.NewSchema(schemaLoader) + schemaLoader := paramsValidator.newReferenceLoader("file:///" + filepath.ToSlash(toOpen)) + loadedSchema, err := paramsValidator.newSchema(schemaLoader) if err != nil { return nil, fmt.Errorf("Failed to load json schema at %s: %v", toOpen, err) } - fileBytes, err := os.ReadFile(fmt.Sprintf("%s/%s", schemaDirectory, fileInfo.Name())) + fileBytes, err := paramsValidator.readFile(fmt.Sprintf("%s/%s", schemaDirectory, fileInfo.Name())) if err != nil { return nil, fmt.Errorf("Failed to read file %s/%s: %v", schemaDirectory, fileInfo.Name(), err) } @@ -543,6 +643,15 @@ func NewBidderParamsValidator(schemaDirectory string) (BidderParamValidator, err schemaContents[BidderName(bidderName)] = string(fileBytes) } + //set alias bidder params schema to its parent + for alias, parent := range aliasBidderToParent { + parentSchema := schemas[parent] + schemas[alias] = parentSchema + + parentSchemaContents := schemaContents[parent] + schemaContents[alias] = parentSchemaContents + } + return &bidderParamValidator{ schemaContents: schemaContents, parsedSchemas: schemas, diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go index f95d4a36540..2176c28e184 100644 --- a/openrtb_ext/bidders_test.go +++ b/openrtb_ext/bidders_test.go @@ -2,7 +2,10 @@ package openrtb_ext import ( "encoding/json" + "errors" + "os" "testing" + "testing/fstest" "github.com/stretchr/testify/assert" "github.com/xeipuuv/gojsonschema" @@ -125,3 +128,155 @@ func TestIsBidderNameReserved(t *testing.T) { assert.Equal(t, test.expected, result, test.bidder) } } + +func TestSetAliasBidderName(t *testing.T) { + parentBidder := BidderName("pBidder") + existingCoreBidderNames := coreBidderNames + + testCases := []struct { + aliasBidderName string + err error + }{ + {"aBidder", nil}, + {"all", errors.New("alias all is a reserved bidder name and cannot be used")}, + } + + for _, test := range testCases { + err := SetAliasBidderName(test.aliasBidderName, parentBidder) + if err != nil { + assert.Equal(t, test.err, err) + } else { + assert.Contains(t, CoreBidderNames(), BidderName(test.aliasBidderName)) + assert.Contains(t, aliasBidderToParent, BidderName(test.aliasBidderName)) + } + } + + //reset package variables to not interfere with other test cases. Example - TestBidderParamSchemas + coreBidderNames = existingCoreBidderNames + aliasBidderToParent = map[BidderName]BidderName{} +} + +type mockParamsHelper struct { + fs fstest.MapFS + absFilePath string + absPathErr error + schemaLoaderErr error + readFileErr error +} + +func (m *mockParamsHelper) readDir(name string) ([]os.DirEntry, error) { + return m.fs.ReadDir(name) +} + +func (m *mockParamsHelper) readFile(name string) ([]byte, error) { + if m.readFileErr != nil { + return nil, m.readFileErr + } + return m.fs.ReadFile(name) +} + +func (m *mockParamsHelper) newReferenceLoader(source string) gojsonschema.JSONLoader { + return nil +} + +func (m *mockParamsHelper) newSchema(l gojsonschema.JSONLoader) (*gojsonschema.Schema, error) { + return nil, m.schemaLoaderErr +} + +func (m *mockParamsHelper) abs(path string) (string, error) { + return m.absFilePath, m.absPathErr +} + +func TestNewBidderParamsValidator(t *testing.T) { + testCases := []struct { + description string + paramsValidator mockParamsHelper + dir string + expectedErr error + }{ + { + description: "Valid case", + paramsValidator: mockParamsHelper{ + fs: fstest.MapFS{ + "test/appnexus.json": { + Data: []byte("{}"), + }, + }, + }, + dir: "test", + }, + { + description: "failed to read directory", + paramsValidator: mockParamsHelper{}, + dir: "t", + expectedErr: errors.New("Failed to read JSON schemas from directory t. open t: file does not exist"), + }, + { + description: "file name does not match the bidder name", + paramsValidator: mockParamsHelper{ + fs: fstest.MapFS{ + "test/anyBidder.json": { + Data: []byte("{}"), + }, + }, + }, + dir: "test", + expectedErr: errors.New("File test/anyBidder.json does not match a valid BidderName."), + }, + { + description: "abs file path error", + paramsValidator: mockParamsHelper{ + fs: fstest.MapFS{ + "test/appnexus.json": { + Data: []byte("{}"), + }, + }, + absFilePath: "test/app.json", + absPathErr: errors.New("any abs error"), + }, + dir: "test", + expectedErr: errors.New("Failed to get an absolute representation of the path: test/app.json, any abs error"), + }, + { + description: "schema loader error", + paramsValidator: mockParamsHelper{ + fs: fstest.MapFS{ + "test/appnexus.json": { + Data: []byte("{}"), + }, + }, + schemaLoaderErr: errors.New("any schema loader error"), + }, + dir: "test", + expectedErr: errors.New("Failed to load json schema at : any schema loader error"), + }, + { + description: "read file error", + paramsValidator: mockParamsHelper{ + fs: fstest.MapFS{ + "test/appnexus.json": { + Data: []byte("{}"), + }, + }, + readFileErr: errors.New("any read file error"), + }, + dir: "test", + expectedErr: errors.New("Failed to read file test/appnexus.json: any read file error"), + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + aliasBidderToParent = map[BidderName]BidderName{"rubicon": "appnexus"} + paramsValidator = &test.paramsValidator + bidderValidator, err := NewBidderParamsValidator(test.dir) + if test.expectedErr == nil { + assert.NoError(t, err) + assert.Contains(t, bidderValidator.Schema("appnexus"), "{}") + assert.Contains(t, bidderValidator.Schema("rubicon"), "{}") + } else { + assert.Equal(t, err, test.expectedErr) + } + }) + } +} diff --git a/openrtb_ext/bidders_validate_test.go b/openrtb_ext/bidders_validate_test.go index 530f260c761..33148d1ed41 100644 --- a/openrtb_ext/bidders_validate_test.go +++ b/openrtb_ext/bidders_validate_test.go @@ -50,7 +50,7 @@ func TestBidderUniquenessGatekeeping(t *testing.T) { // - Exclude duplicates of adapters for the same bidder, as it's unlikely a publisher will use both. var bidders []string for _, bidder := range CoreBidderNames() { - if bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != BidderFreewheelSSPOld { + if bidder != BidderSilverPush && bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != BidderFreewheelSSPOld && bidder != BidderYahooAdvertising { bidders = append(bidders, string(bidder)) } } diff --git a/openrtb_ext/deal_tier.go b/openrtb_ext/deal_tier.go index ef85b9b1df8..45285d21663 100644 --- a/openrtb_ext/deal_tier.go +++ b/openrtb_ext/deal_tier.go @@ -27,20 +27,6 @@ func ReadDealTiersFromImp(imp openrtb2.Imp) (DealTierBidderMap, error) { return dealTiers, nil } - // imp.ext.{bidder} - var impExt map[string]struct { - DealTier *DealTier `json:"dealTier"` - } - if err := json.Unmarshal(imp.Ext, &impExt); err != nil { - return nil, err - } - for bidder, param := range impExt { - if param.DealTier != nil { - dealTiers[BidderName(bidder)] = *param.DealTier - } - } - - // imp.ext.prebid.{bidder} var impPrebidExt struct { Prebid struct { Bidders map[string]struct { diff --git a/openrtb_ext/deal_tier_test.go b/openrtb_ext/deal_tier_test.go index 6aaebbab687..0046b788ece 100644 --- a/openrtb_ext/deal_tier_test.go +++ b/openrtb_ext/deal_tier_test.go @@ -31,55 +31,50 @@ func TestReadDealTiersFromImp(t *testing.T) { expectedResult: DealTierBidderMap{}, }, { - description: "imp.ext - with other params", - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}`), - expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}}, + description: "imp.ext - no prebid but with other params", + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}, "tid": "1234"}`), + expectedResult: DealTierBidderMap{}, }, { - description: "imp.ext - multiple", - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "appnexusPrefix"}, "placementId": 12345}, "rubicon": {"dealTier": {"minDealTier": 8, "prefix": "rubiconPrefix"}, "placementId": 12345}}`), - expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "appnexusPrefix", MinDealTier: 5}, BidderRubicon: {Prefix: "rubiconPrefix", MinDealTier: 8}}, + description: "imp.ext.prebid - nil", + impExt: json.RawMessage(`{"prebid": null}`), + expectedResult: DealTierBidderMap{}, }, { - description: "imp.ext - no deal tier", - impExt: json.RawMessage(`{"appnexus": {"placementId": 12345}}`), + description: "imp.ext.prebid - empty", + impExt: json.RawMessage(`{"prebid": {}}`), expectedResult: DealTierBidderMap{}, }, { - description: "imp.ext - error", - impExt: json.RawMessage(`{"appnexus": {"dealTier": "wrong type", "placementId": 12345}}`), - expectedError: "json: cannot unmarshal string into Go struct field .dealTier of type openrtb_ext.DealTier", + description: "imp.ext.prebid - no bidder but with other params", + impExt: json.RawMessage(`{"prebid": {"supportdeals": true}}`), + expectedResult: DealTierBidderMap{}, }, { - description: "imp.ext.prebid", + description: "imp.ext.prebid.bidder - one", impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}}}`), expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}}, }, { - description: "imp.ext.prebid- multiple", + description: "imp.ext.prebid.bidder - one with other params", + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}, "supportdeals": true}, "tid": "1234"}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}}, + }, + { + description: "imp.ext.prebid.bidder - multiple", impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "appnexusPrefix"}, "placementId": 12345}, "rubicon": {"dealTier": {"minDealTier": 8, "prefix": "rubiconPrefix"}, "placementId": 12345}}}}`), expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "appnexusPrefix", MinDealTier: 5}, BidderRubicon: {Prefix: "rubiconPrefix", MinDealTier: 8}}, }, { - description: "imp.ext.prebid - no deal tier", + description: "imp.ext.prebid.bidder - one without deal tier", impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"placementId": 12345}}}}`), expectedResult: DealTierBidderMap{}, }, { - description: "imp.ext.prebid - error", + description: "imp.ext.prebid.bidder - error", impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": "wrong type", "placementId": 12345}}}}`), expectedError: "json: cannot unmarshal string into Go struct field .prebid.bidder.dealTier of type openrtb_ext.DealTier", }, - { - description: "imp.ext.prebid wins over imp.ext", - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "impExt"}, "placementId": 12345}, "prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 8, "prefix": "impExtPrebid"}, "placementId": 12345}}}}`), - expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "impExtPrebid", MinDealTier: 8}}, - }, - { - description: "imp.ext.prebid coexists with imp.ext", - impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "impExt"}, "placementId": 12345}, "prebid": {"bidder": {"rubicon": {"dealTier": {"minDealTier": 8, "prefix": "impExtPrebid"}, "placementId": 12345}}}}`), - expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "impExt", MinDealTier: 5}, BidderRubicon: {Prefix: "impExtPrebid", MinDealTier: 8}}, - }, } for _, test := range testCases { diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index 409f9de9f11..3553946cc2e 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -31,6 +31,46 @@ type PriceFloorRules struct { PriceFloorLocation string `json:"location,omitempty"` } +// GetEnforcePBS will check if floors enforcement is enabled in request +func (Floors *PriceFloorRules) GetEnforcePBS() bool { + if Floors != nil && Floors.Enforcement != nil && Floors.Enforcement.EnforcePBS != nil { + return *Floors.Enforcement.EnforcePBS + } + return true +} + +// GetFloorsSkippedFlag will return floors skipped flag +func (Floors *PriceFloorRules) GetFloorsSkippedFlag() bool { + if Floors != nil && Floors.Skipped != nil { + return *Floors.Skipped + } + return false +} + +// GetEnforceRate will return enforcement rate in request +func (Floors *PriceFloorRules) GetEnforceRate() int { + if Floors != nil && Floors.Enforcement != nil { + return Floors.Enforcement.EnforceRate + } + return 0 +} + +// GetEnforceDealsFlag will return FloorDeals flag in request +func (Floors *PriceFloorRules) GetEnforceDealsFlag() bool { + if Floors != nil && Floors.Enforcement != nil && Floors.Enforcement.FloorDeals != nil { + return *Floors.Enforcement.FloorDeals + } + return false +} + +// GetEnabled will check if floors is enabled in request +func (Floors *PriceFloorRules) GetEnabled() bool { + if Floors != nil && Floors.Enabled != nil { + return *Floors.Enabled + } + return true +} + type PriceFloorEndpoint struct { URL string `json:"url,omitempty"` } @@ -53,6 +93,28 @@ type PriceFloorModelGroup struct { Values map[string]float64 `json:"values,omitempty"` Default float64 `json:"default,omitempty"` } + +func (mg PriceFloorModelGroup) Copy() PriceFloorModelGroup { + newMg := new(PriceFloorModelGroup) + newMg.Currency = mg.Currency + newMg.ModelVersion = mg.ModelVersion + newMg.SkipRate = mg.SkipRate + newMg.Default = mg.Default + if mg.ModelWeight != nil { + newMg.ModelWeight = new(int) + *newMg.ModelWeight = *mg.ModelWeight + } + + newMg.Schema.Delimiter = mg.Schema.Delimiter + newMg.Schema.Fields = make([]string, len(mg.Schema.Fields)) + copy(newMg.Schema.Fields, mg.Schema.Fields) + newMg.Values = make(map[string]float64, len(mg.Values)) + for key, val := range mg.Values { + newMg.Values[key] = val + } + return *newMg +} + type PriceFloorSchema struct { Fields []string `json:"fields,omitempty"` Delimiter string `json:"delimiter,omitempty"` @@ -83,32 +145,3 @@ type ExtImp struct { type ImpExtPrebid struct { Floors Price `json:"floors,omitempty"` } - -// GetEnabled will check if floors is enabled in request -func (Floors *PriceFloorRules) GetEnabled() bool { - if Floors != nil && Floors.Enabled != nil { - return *Floors.Enabled - } - return true -} - -func (modelGroup PriceFloorModelGroup) Copy() PriceFloorModelGroup { - newModelGroup := new(PriceFloorModelGroup) - newModelGroup.Currency = modelGroup.Currency - newModelGroup.ModelVersion = modelGroup.ModelVersion - newModelGroup.SkipRate = modelGroup.SkipRate - newModelGroup.Default = modelGroup.Default - if modelGroup.ModelWeight != nil { - newModelGroup.ModelWeight = new(int) - *newModelGroup.ModelWeight = *modelGroup.ModelWeight - } - - newModelGroup.Schema.Delimiter = modelGroup.Schema.Delimiter - newModelGroup.Schema.Fields = make([]string, len(modelGroup.Schema.Fields)) - copy(newModelGroup.Schema.Fields, modelGroup.Schema.Fields) - newModelGroup.Values = make(map[string]float64, len(modelGroup.Values)) - for key, val := range modelGroup.Values { - newModelGroup.Values[key] = val - } - return *newModelGroup -} diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go new file mode 100644 index 00000000000..20247736768 --- /dev/null +++ b/openrtb_ext/floors_test.go @@ -0,0 +1,220 @@ +package openrtb_ext + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func getFlag(in bool) *bool { + return &in +} + +func TestPriceFloorRulesGetEnforcePBS(t *testing.T) { + tests := []struct { + name string + floors *PriceFloorRules + want bool + }{ + { + name: "EnforcePBS_Enabled", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{ + EnforcePBS: getFlag(true), + }, + }, + want: true, + }, + { + name: "EnforcePBS_NotProvided", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{}, + }, + want: true, + }, + { + name: "EnforcePBS_Disabled", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{ + EnforcePBS: getFlag(false), + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.floors.GetEnforcePBS() + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestPriceFloorRulesGetFloorsSkippedFlag(t *testing.T) { + tests := []struct { + name string + floors *PriceFloorRules + want bool + }{ + { + name: "Skipped_true", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Skipped: getFlag(true), + }, + want: true, + }, + { + name: "Skipped_false", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Skipped: getFlag(false), + }, + want: false, + }, + { + name: "Skipped_NotProvided", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.floors.GetFloorsSkippedFlag() + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestPriceFloorRulesGetEnforceRate(t *testing.T) { + tests := []struct { + name string + floors *PriceFloorRules + want int + }{ + { + name: "EnforceRate_100", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{ + EnforcePBS: getFlag(true), + EnforceRate: 100, + }, + }, + want: 100, + }, + { + name: "EnforceRate_0", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{ + EnforcePBS: getFlag(true), + EnforceRate: 0, + }, + }, + want: 0, + }, + { + name: "EnforceRate_NotProvided", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + }, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.floors.GetEnforceRate() + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestPriceFloorRulesGetEnforceDealsFlag(t *testing.T) { + tests := []struct { + name string + floors *PriceFloorRules + want bool + }{ + { + name: "FloorDeals_true", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{ + EnforcePBS: getFlag(true), + EnforceRate: 0, + FloorDeals: getFlag(true), + }, + }, + want: true, + }, + { + name: "FloorDeals_false", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + Enforcement: &PriceFloorEnforcement{ + EnforcePBS: getFlag(true), + FloorDeals: getFlag(false), + }, + Skipped: getFlag(false), + }, + want: false, + }, + { + name: "FloorDeals_NotProvided", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.floors.GetEnforceDealsFlag() + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestPriceFloorRulesGetEnabled(t *testing.T) { + tests := []struct { + name string + floors *PriceFloorRules + want bool + }{ + { + name: "Enabled_true", + floors: &PriceFloorRules{ + Enabled: getFlag(true), + }, + want: true, + }, + { + name: "Enabled_false", + floors: &PriceFloorRules{ + Enabled: getFlag(false), + }, + want: false, + }, + { + name: "Enabled_NotProvided", + floors: &PriceFloorRules{}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.floors.GetEnabled() + assert.Equal(t, tt.want, got, tt.name) + }) + } +} diff --git a/openrtb_ext/imp_adnuntius.go b/openrtb_ext/imp_adnuntius.go index b1e41e806b6..86023c48231 100644 --- a/openrtb_ext/imp_adnuntius.go +++ b/openrtb_ext/imp_adnuntius.go @@ -3,5 +3,6 @@ package openrtb_ext type ImpExtAdnunitus struct { Auid string `json:"auId"` Network string `json:"network"` - NoCookies bool `json:noCookies` + NoCookies bool `json:"noCookies"` + MaxDeals int `json:"maxDeals"` } diff --git a/openrtb_ext/imp_adocean.go b/openrtb_ext/imp_adocean.go index e690e929778..1dd64d284e2 100644 --- a/openrtb_ext/imp_adocean.go +++ b/openrtb_ext/imp_adocean.go @@ -1,7 +1,7 @@ package openrtb_ext type ExtImpAdOcean struct { - EmitterDomain string `json:"emiter"` + EmitterPrefix string `json:"emitterPrefix"` MasterID string `json:"masterId"` SlaveID string `json:"slaveId"` } diff --git a/openrtb_ext/imp_adquery.go b/openrtb_ext/imp_adquery.go new file mode 100644 index 00000000000..73477e784b1 --- /dev/null +++ b/openrtb_ext/imp_adquery.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtAdQuery struct { + PlacementID string `json:"placementId"` + Type string `json:"type"` +} diff --git a/openrtb_ext/imp_aidem.go b/openrtb_ext/imp_aidem.go new file mode 100644 index 00000000000..59457f1eb4a --- /dev/null +++ b/openrtb_ext/imp_aidem.go @@ -0,0 +1,8 @@ +package openrtb_ext + +type ImpExtFoo struct { + SiteID string `json:"siteId"` + PublisherID string `json:"publisherId"` + PlacementID string `json:"placementId"` + RateLimit string `json:"rateLimit"` +} diff --git a/openrtb_ext/imp_appnexus.go b/openrtb_ext/imp_appnexus.go index 56dc0dbb8ed..d9549e74750 100644 --- a/openrtb_ext/imp_appnexus.go +++ b/openrtb_ext/imp_appnexus.go @@ -2,31 +2,94 @@ package openrtb_ext import ( "encoding/json" + "fmt" + "strings" "github.com/prebid/prebid-server/util/jsonutil" ) // ExtImpAppnexus defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus type ExtImpAppnexus struct { - DeprecatedPlacementId jsonutil.StringInt `json:"placementId"` - LegacyInvCode string `json:"invCode"` - LegacyTrafficSourceCode string `json:"trafficSourceCode"` - PlacementId jsonutil.StringInt `json:"placement_id"` - InvCode string `json:"inv_code"` - Member string `json:"member"` - Keywords []*ExtImpAppnexusKeyVal `json:"keywords"` - TrafficSourceCode string `json:"traffic_source_code"` - Reserve float64 `json:"reserve"` - Position string `json:"position"` - UsePaymentRule *bool `json:"use_pmt_rule"` - DeprecatedUsePaymentRule *bool `json:"use_payment_rule"` + DeprecatedPlacementId jsonutil.StringInt `json:"placementId"` + LegacyInvCode string `json:"invCode"` + LegacyTrafficSourceCode string `json:"trafficSourceCode"` + PlacementId jsonutil.StringInt `json:"placement_id"` + InvCode string `json:"inv_code"` + Member string `json:"member"` + Keywords ExtImpAppnexusKeywords `json:"keywords"` + TrafficSourceCode string `json:"traffic_source_code"` + Reserve float64 `json:"reserve"` + Position string `json:"position"` + UsePaymentRule *bool `json:"use_pmt_rule"` + DeprecatedUsePaymentRule *bool `json:"use_payment_rule"` // At this time we do no processing on the private sizes, so just leaving it as a JSON blob. - PrivateSizes json.RawMessage `json:"private_sizes"` - AdPodId bool `json:"generate_ad_pod_id"` + PrivateSizes json.RawMessage `json:"private_sizes"` + AdPodId bool `json:"generate_ad_pod_id"` + ExtInvCode string `json:"ext_inv_code"` + ExternalImpId string `json:"external_imp_id"` } -// ExtImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus.keywords[i] -type ExtImpAppnexusKeyVal struct { +type ExtImpAppnexusKeywords string + +// extImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus.keywords[i] +type extImpAppnexusKeyVal struct { Key string `json:"key,omitempty"` Values []string `json:"value,omitempty"` } + +func (ks *ExtImpAppnexusKeywords) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return nil + } + + switch b[0] { + case '{': + var results map[string][]string + if err := json.Unmarshal(b, &results); err != nil { + return err + } + + var keywords strings.Builder + for key, values := range results { + if len(values) == 0 { + keywords.WriteString(fmt.Sprintf("%s,", key)) + } else { + for _, val := range values { + keywords.WriteString(fmt.Sprintf("%s=%s,", key, val)) + } + } + } + if len(keywords.String()) > 0 { + *ks = ExtImpAppnexusKeywords(keywords.String()[:keywords.Len()-1]) + } + case '[': + var results []extImpAppnexusKeyVal + if err := json.Unmarshal(b, &results); err != nil { + return err + } + var kvs strings.Builder + for _, kv := range results { + if len(kv.Values) == 0 { + kvs.WriteString(fmt.Sprintf("%s,", kv.Key)) + } else { + for _, val := range kv.Values { + kvs.WriteString(fmt.Sprintf("%s=%s,", kv.Key, val)) + } + } + } + if len(kvs.String()) > 0 { + *ks = ExtImpAppnexusKeywords(kvs.String()[:kvs.Len()-1]) + } + case '"': + var keywords string + if err := json.Unmarshal(b, &keywords); err != nil { + return err + } + *ks = ExtImpAppnexusKeywords(keywords) + } + return nil +} + +func (ks *ExtImpAppnexusKeywords) String() string { + return *(*string)(ks) +} diff --git a/openrtb_ext/imp_appnexus_test.go b/openrtb_ext/imp_appnexus_test.go new file mode 100644 index 00000000000..cbd6779d5da --- /dev/null +++ b/openrtb_ext/imp_appnexus_test.go @@ -0,0 +1,47 @@ +package openrtb_ext + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeywordsUnmarshalJSON(t *testing.T) { + type keywords struct { + Keywords ExtImpAppnexusKeywords `json:"keywords"` + } + + type testCase struct { + input []byte + expected string + desc string + } + + validTestCases := []testCase{ + {input: []byte(`{"keywords" : { "pets": ["dog"] }}`), expected: "pets=dog", desc: "dynamic json object"}, + {input: []byte(`{"keywords" : { "foo":[] }}`), expected: "foo", desc: "dynamic json object with empty value array"}, + {input: []byte(`{"keywords" : [{"key": "foo", "value": ["bar","baz"]},{"key": "valueless"}]}`), expected: "foo=bar,foo=baz,valueless", desc: "array of objects"}, + {input: []byte(`{"keywords" : "foo=bar,foo=baz,valueless"}`), expected: "foo=bar,foo=baz,valueless", desc: "string keywords"}, + {input: []byte(`{"keywords" : ""}`), expected: "", desc: "empty string"}, + {input: []byte(`{"keywords" : {}}`), expected: "", desc: "empty keywords object"}, + {input: []byte(`{"keywords" : [{}]}`), expected: "", desc: "empty keywords object array"}, + {input: []byte(`{"keywords": []}`), expected: "", desc: "empty keywords array"}, + } + + for _, test := range validTestCases { + var keywords keywords + assert.NoError(t, json.Unmarshal(test.input, &keywords), test.desc) + assert.Equal(t, test.expected, keywords.Keywords.String()) + } + + invalidTestCases := []testCase{ + {input: []byte(`{"keywords": [{]}`), desc: "invalid keywords array"}, + {input: []byte(`{"keywords" : {"}}`), desc: "invalid keywords object"}, + } + + for _, test := range invalidTestCases { + var keywords keywords + assert.Error(t, json.Unmarshal(test.input, &keywords), test.desc) + } +} diff --git a/openrtb_ext/imp_axis.go b/openrtb_ext/imp_axis.go new file mode 100644 index 00000000000..0381e4c5ce5 --- /dev/null +++ b/openrtb_ext/imp_axis.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtAxis struct { + Integration string `json:"integration,omitempty"` + Token string `json:"token,omitempty"` +} diff --git a/openrtb_ext/imp_bematterfull.go b/openrtb_ext/imp_bematterfull.go new file mode 100644 index 00000000000..bc3ca451a61 --- /dev/null +++ b/openrtb_ext/imp_bematterfull.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtBematterfull struct { + Env string `json:"env"` + Pid string `json:"pid"` +} diff --git a/openrtb_ext/imp_bluesea.go b/openrtb_ext/imp_bluesea.go new file mode 100644 index 00000000000..d1acea9dac1 --- /dev/null +++ b/openrtb_ext/imp_bluesea.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpBluesea struct { + PubId string `json:"pubid"` + Token string `json:"token"` +} diff --git a/openrtb_ext/imp_brightroll.go b/openrtb_ext/imp_brightroll.go deleted file mode 100644 index bd42d4aae5d..00000000000 --- a/openrtb_ext/imp_brightroll.go +++ /dev/null @@ -1,6 +0,0 @@ -package openrtb_ext - -// ExtImpBrightroll defines the contract for bidrequest.imp[i].ext.prebid.bidder.brightroll -type ExtImpBrightroll struct { - Publisher string `json:"publisher"` -} diff --git a/openrtb_ext/imp_emx_digital.go b/openrtb_ext/imp_cadent_aperture_mx.go similarity index 72% rename from openrtb_ext/imp_emx_digital.go rename to openrtb_ext/imp_cadent_aperture_mx.go index 5c5956b1fc1..fc4280ff491 100644 --- a/openrtb_ext/imp_emx_digital.go +++ b/openrtb_ext/imp_cadent_aperture_mx.go @@ -1,6 +1,6 @@ package openrtb_ext -type ExtImpEmxDigital struct { +type ExtImpCadentApertureMX struct { TagID string `json:"tagid"` BidFloor string `json:"bidfloor,omitempty"` } diff --git a/openrtb_ext/imp_datablocks.go b/openrtb_ext/imp_datablocks.go index c8659d3ff11..dc3bae3f3fe 100644 --- a/openrtb_ext/imp_datablocks.go +++ b/openrtb_ext/imp_datablocks.go @@ -2,6 +2,5 @@ package openrtb_ext // ExtImpDatablocks defines the contract for bidrequest.imp[i].ext.prebid.bidder.datablocks type ExtImpDatablocks struct { - SourceId int `json:"sourceId"` - Host string `json:"host"` + SourceId int `json:"sourceId"` } diff --git a/openrtb_ext/imp_emtv.go b/openrtb_ext/imp_emtv.go new file mode 100644 index 00000000000..a77010742a0 --- /dev/null +++ b/openrtb_ext/imp_emtv.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtEmtv struct { + PlacementID string `json:"placementId"` + EndpointID string `json:"endpointId"` +} diff --git a/openrtb_ext/imp_flipp.go b/openrtb_ext/imp_flipp.go new file mode 100644 index 00000000000..56874c024c6 --- /dev/null +++ b/openrtb_ext/imp_flipp.go @@ -0,0 +1,16 @@ +package openrtb_ext + +type ImpExtFlipp struct { + PublisherNameIdentifier string `json:"publisherNameIdentifier"` + CreativeType string `json:"creativeType"` + SiteID int64 `json:"siteId"` + ZoneIds []int64 `json:"zoneIds,omitempty"` + UserKey string `json:"userKey,omitempty"` + Options ImpExtFlippOptions `json:"options,omitempty"` +} + +type ImpExtFlippOptions struct { + StartCompact bool `json:"startCompact,omitempty"` + DwellExpand bool `json:"dwellExpand,omitempty"` + ContentCode string `json:"contentCode,omitempty"` +} diff --git a/openrtb_ext/imp_freewheelssp.go b/openrtb_ext/imp_freewheelssp.go index 11c22ff8154..110f018f512 100644 --- a/openrtb_ext/imp_freewheelssp.go +++ b/openrtb_ext/imp_freewheelssp.go @@ -1,5 +1,9 @@ package openrtb_ext +import ( + "github.com/prebid/prebid-server/util/jsonutil" +) + type ImpExtFreewheelSSP struct { - ZoneId int `json:"zoneId"` + ZoneId jsonutil.StringInt `json:"zoneId"` } diff --git a/openrtb_ext/imp_frvradn.go b/openrtb_ext/imp_frvradn.go new file mode 100644 index 00000000000..ac8f86b22b2 --- /dev/null +++ b/openrtb_ext/imp_frvradn.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtFRVRAdn struct { + PublisherID string `json:"publisher_id"` + AdUnitID string `json:"ad_unit_id"` +} diff --git a/openrtb_ext/imp_gothamads.go b/openrtb_ext/imp_gothamads.go new file mode 100644 index 00000000000..fb945fbba67 --- /dev/null +++ b/openrtb_ext/imp_gothamads.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtGothamAds struct { + AccountID string `json:"accountId"` +} diff --git a/openrtb_ext/imp_ix.go b/openrtb_ext/imp_ix.go index 9f977fb0dcd..40c3f51867f 100644 --- a/openrtb_ext/imp_ix.go +++ b/openrtb_ext/imp_ix.go @@ -4,4 +4,5 @@ package openrtb_ext type ExtImpIx struct { SiteId string `json:"siteId"` Size []int `json:"size"` + Sid string `json:"sid"` } diff --git a/openrtb_ext/imp_kargo.go b/openrtb_ext/imp_kargo.go index 682561fb1f0..f01e602a0f6 100644 --- a/openrtb_ext/imp_kargo.go +++ b/openrtb_ext/imp_kargo.go @@ -1,5 +1,6 @@ package openrtb_ext type ImpExtKargo struct { - AdSlotID string `json:"adSlotID"` + PlacementId string `json:"placementId"` + AdSlotID string `json:"adSlotID"` // Deprecated - Use `placementId` } diff --git a/openrtb_ext/imp_liftoff.go b/openrtb_ext/imp_liftoff.go new file mode 100644 index 00000000000..d8a93d8906a --- /dev/null +++ b/openrtb_ext/imp_liftoff.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ImpExtLiftoff struct { + BidToken string `json:"bid_token"` + PubAppStoreID string `json:"app_store_id"` + PlacementRefID string `json:"placement_reference_id"` +} diff --git a/openrtb_ext/imp_lmkiviads.go b/openrtb_ext/imp_lmkiviads.go new file mode 100644 index 00000000000..8638749b1b3 --- /dev/null +++ b/openrtb_ext/imp_lmkiviads.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtLmKiviads struct { + Env string `json:"env"` + Pid string `json:"pid"` +} diff --git a/openrtb_ext/imp_mgidX.go b/openrtb_ext/imp_mgidX.go new file mode 100644 index 00000000000..1a16fcc4934 --- /dev/null +++ b/openrtb_ext/imp_mgidX.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtMgidX struct { + PlacementID string `json:"placementId"` + EndpointID string `json:"endpointId"` +} diff --git a/openrtb_ext/imp_motorik.go b/openrtb_ext/imp_motorik.go new file mode 100644 index 00000000000..0b6fca78b38 --- /dev/null +++ b/openrtb_ext/imp_motorik.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtMotorik struct { + AccountID string `json:"accountId"` + PlacementID string `json:"placementId"` +} diff --git a/openrtb_ext/imp_ownadx.go b/openrtb_ext/imp_ownadx.go new file mode 100644 index 00000000000..caac600b50e --- /dev/null +++ b/openrtb_ext/imp_ownadx.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpOwnAdx struct { + SspId string `json:"sspId"` + SeatId string `json:"seatId"` + TokenId string `json:"tokenId"` +} diff --git a/openrtb_ext/imp_rise.go b/openrtb_ext/imp_rise.go new file mode 100644 index 00000000000..7311f05d52d --- /dev/null +++ b/openrtb_ext/imp_rise.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ImpExtRise defines the contract for bidrequest.imp[i].ext.prebid.bidder.rise +type ImpExtRise struct { + PublisherID string `json:"publisher_id"` + Org string `json:"org"` +} diff --git a/openrtb_ext/imp_rtbhouse.go b/openrtb_ext/imp_rtbhouse.go new file mode 100644 index 00000000000..747ea158f9d --- /dev/null +++ b/openrtb_ext/imp_rtbhouse.go @@ -0,0 +1,10 @@ +package openrtb_ext + +// ExtImpRTBHouse defines the contract for bidrequest.imp[i].ext.prebid.bidder.rtbhouse +type ExtImpRTBHouse struct { + PublisherId string `json:"publisherId"` + Region string `json:"region"` + + BidFloor float64 `json:"bidfloor,omitempty"` + Channel string `json:"channel,omitempty"` +} diff --git a/openrtb_ext/imp_sa_lunamedia.go b/openrtb_ext/imp_sa_lunamedia.go index cb99b0ac561..5b2d8a10d15 100644 --- a/openrtb_ext/imp_sa_lunamedia.go +++ b/openrtb_ext/imp_sa_lunamedia.go @@ -2,5 +2,5 @@ package openrtb_ext type ExtImpSaLunamedia struct { Key string `json:"key"` - Type string `json:"type,omitempty"` + Type string `json:"type"` } diff --git a/openrtb_ext/imp_screencore.go b/openrtb_ext/imp_screencore.go new file mode 100644 index 00000000000..5a3fa1c39ff --- /dev/null +++ b/openrtb_ext/imp_screencore.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtScreencore struct { + AccountID string `json:"accountId"` + PlacementID string `json:"placementId"` +} diff --git a/openrtb_ext/imp_silverpush.go b/openrtb_ext/imp_silverpush.go new file mode 100644 index 00000000000..0a043356332 --- /dev/null +++ b/openrtb_ext/imp_silverpush.go @@ -0,0 +1,8 @@ +package openrtb_ext + +// ImpExtSilverpush defines the contract for bidrequest.imp[i].ext.prebid.bidder.silverpush +// PublisherId is mandatory parameters +type ImpExtSilverpush struct { + PublisherId string `json:"publisherId"` + BidFloor float64 `json:"bidfloor"` +} diff --git a/openrtb_ext/imp_smaato.go b/openrtb_ext/imp_smaato.go index bb92b318416..fb326100f8b 100644 --- a/openrtb_ext/imp_smaato.go +++ b/openrtb_ext/imp_smaato.go @@ -1,12 +1,19 @@ package openrtb_ext +import "encoding/json" + // ExtImpSmaato defines the contract for bidrequest.imp[i].ext.prebid.bidder.smaato // PublisherId and AdSpaceId are mandatory parameters for non adpod (long-form video) requests, others are optional parameters // PublisherId and AdBreakId are mandatory parameters for adpod (long-form video) requests, others are optional parameters -// AdSpaceId is identifier for specific ad placement or ad tag -// AdBreakId is identifier for specific ad placement or ad tag +// AdSpaceId is the identifier for specific ad placement or ad tag +// AdBreakId is the identifier for specific ad placement or ad tag type ExtImpSmaato struct { PublisherID string `json:"publisherId"` AdSpaceID string `json:"adspaceId"` AdBreakID string `json:"adbreakId"` } + +// ExtImpExtraDataSmaato defines extra properties from imp[i].ext object +type ExtImpExtraDataSmaato struct { + Skadn json.RawMessage `json:"skadn,omitempty"` +} diff --git a/openrtb_ext/imp_tpmn.go b/openrtb_ext/imp_tpmn.go new file mode 100644 index 00000000000..373b3089cc8 --- /dev/null +++ b/openrtb_ext/imp_tpmn.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpTpmn defines TPMN specifiec param +type ExtImpTpmn struct { + InventoryId int `json:"inventoryId"` +} diff --git a/openrtb_ext/imp_verizonmedia.go b/openrtb_ext/imp_verizonmedia.go deleted file mode 100644 index 2ae8e5ea70a..00000000000 --- a/openrtb_ext/imp_verizonmedia.go +++ /dev/null @@ -1,7 +0,0 @@ -package openrtb_ext - -// ExtImpVerizonMedia defines the contract for bidrequest.imp[i].ext.prebid.bidder.verizonmedia -type ExtImpVerizonMedia struct { - Dcn string `json:"dcn"` - Pos string `json:"pos"` -} diff --git a/openrtb_ext/imp_vox.go b/openrtb_ext/imp_vox.go new file mode 100644 index 00000000000..a61525077b3 --- /dev/null +++ b/openrtb_ext/imp_vox.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ImpExtVox struct { + PlacementID string `json:"placementId"` + ImageUrl string `json:"imageUrl"` + DisplaySizes []string `json:"displaySizes"` +} diff --git a/openrtb_ext/imp_xeworks.go b/openrtb_ext/imp_xeworks.go new file mode 100644 index 00000000000..dac3b6d7365 --- /dev/null +++ b/openrtb_ext/imp_xeworks.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtXeworks struct { + Env string `json:"env"` + Pid string `json:"pid"` +} diff --git a/openrtb_ext/imp_yahooAds.go b/openrtb_ext/imp_yahooAds.go new file mode 100644 index 00000000000..36a4a0f618b --- /dev/null +++ b/openrtb_ext/imp_yahooAds.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpYahooAds defines the contract for bidrequest.imp[i].ext.prebid.bidder.yahooAds +type ExtImpYahooAds struct { + Dcn string `json:"dcn"` + Pos string `json:"pos"` +} diff --git a/openrtb_ext/imp_yahooAdvertising.go b/openrtb_ext/imp_yahooAdvertising.go new file mode 100644 index 00000000000..e0e8614a0f9 --- /dev/null +++ b/openrtb_ext/imp_yahooAdvertising.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpYahooAdvertising defines the contract for bidrequest.imp[i].ext.prebid.bidder.yahooAdvertising +type ExtImpYahooAdvertising struct { + Dcn string `json:"dcn"` + Pos string `json:"pos"` +} diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 80a678fbf77..f5418ba4d1f 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -5,6 +5,9 @@ import ( "fmt" "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/maputil" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/prebid/prebid-server/util/sliceutil" ) // FirstPartyDataExtKey defines a field name within request.ext and request.imp.ext reserved for first party data. @@ -38,45 +41,54 @@ type ExtRequest struct { // ExtRequestPrebid defines the contract for bidrequest.ext.prebid type ExtRequestPrebid struct { - Aliases map[string]string `json:"aliases,omitempty"` - AliasGVLIDs map[string]uint16 `json:"aliasgvlids,omitempty"` - BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` - BidderConfigs []BidderConfig `json:"bidderconfig,omitempty"` - BidderParams json.RawMessage `json:"bidderparams,omitempty"` - Cache *ExtRequestPrebidCache `json:"cache,omitempty"` - Channel *ExtRequestPrebidChannel `json:"channel,omitempty"` - CurrencyConversions *ExtRequestCurrency `json:"currency,omitempty"` - Data *ExtRequestPrebidData `json:"data,omitempty"` - Debug bool `json:"debug,omitempty"` - Events json.RawMessage `json:"events,omitempty"` - Experiment *Experiment `json:"experiment,omitempty"` - Integration string `json:"integration,omitempty"` - MultiBid []*ExtMultiBid `json:"multibid,omitempty"` - Passthrough json.RawMessage `json:"passthrough,omitempty"` - SChains []*ExtRequestPrebidSChain `json:"schains,omitempty"` - Server *ExtRequestPrebidServer `json:"server,omitempty"` - StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` - SupportDeals bool `json:"supportdeals,omitempty"` - Targeting *ExtRequestTargeting `json:"targeting,omitempty"` + AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"` + Aliases map[string]string `json:"aliases,omitempty"` + AliasGVLIDs map[string]uint16 `json:"aliasgvlids,omitempty"` + BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` + BidAdjustments *ExtRequestPrebidBidAdjustments `json:"bidadjustments,omitempty"` + BidderConfigs []BidderConfig `json:"bidderconfig,omitempty"` + BidderParams json.RawMessage `json:"bidderparams,omitempty"` + Cache *ExtRequestPrebidCache `json:"cache,omitempty"` + Channel *ExtRequestPrebidChannel `json:"channel,omitempty"` + CurrencyConversions *ExtRequestCurrency `json:"currency,omitempty"` + Data *ExtRequestPrebidData `json:"data,omitempty"` + Debug bool `json:"debug,omitempty"` + Events json.RawMessage `json:"events,omitempty"` + Experiment *Experiment `json:"experiment,omitempty"` + Floors *PriceFloorRules `json:"floors,omitempty"` + Integration string `json:"integration,omitempty"` + MultiBid []*ExtMultiBid `json:"multibid,omitempty"` + MultiBidMap map[string]ExtMultiBid `json:"-"` + Passthrough json.RawMessage `json:"passthrough,omitempty"` + SChains []*ExtRequestPrebidSChain `json:"schains,omitempty"` + Sdk *ExtRequestSdk `json:"sdk,omitempty"` + Server *ExtRequestPrebidServer `json:"server,omitempty"` + StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` + SupportDeals bool `json:"supportdeals,omitempty"` + Targeting *ExtRequestTargeting `json:"targeting,omitempty"` + + //AlternateBidderCodes is populated with host's AlternateBidderCodes config if not defined in request + AlternateBidderCodes *ExtAlternateBidderCodes `json:"alternatebiddercodes,omitempty"` + + // Macros specifies list of custom macros along with the values. This is used while forming + // the tracker URLs, where PBS will replace the Custom Macro with its value with url-encoding + Macros map[string]string `json:"macros,omitempty"` // NoSale specifies bidders with whom the publisher has a legal relationship where the // passing of personally identifiable information doesn't constitute a sale per CCPA law. // The array may contain a single sstar ('*') entry to represent all bidders. NoSale []string `json:"nosale,omitempty"` - //AlternateBidderCodes is populated with host's AlternateBidderCodes config if not defined in request - AlternateBidderCodes *ExtAlternateBidderCodes `json:"alternatebiddercodes,omitempty"` + // ReturnAllBidStatus if true populates bidresponse.ext.prebid.seatnonbid with all bids which was + // either rejected, nobid, input error + ReturnAllBidStatus bool `json:"returnallbidstatus,omitempty"` - Floors *PriceFloorRules `json:"floors,omitempty"` - MultiBidMap map[string]ExtMultiBid `json:"-"` // Trace controls the level of detail in the output information returned from executing hooks. // There are two options: // - verbose: sets maximum level of output information // - basic: excludes debugmessages and analytic_tags from output // any other value or an empty string disables trace output at all. Trace string `json:"trace,omitempty"` - - AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"` } type AdServerTarget struct { @@ -105,9 +117,9 @@ type Config struct { } type ORTB2 struct { //First party data - Site map[string]json.RawMessage `json:"site,omitempty"` - App map[string]json.RawMessage `json:"app,omitempty"` - User map[string]json.RawMessage `json:"user,omitempty"` + Site json.RawMessage `json:"site,omitempty"` + App json.RawMessage `json:"app,omitempty"` + User json.RawMessage `json:"user,omitempty"` } type ExtRequestCurrency struct { @@ -149,16 +161,43 @@ type ExtRequestPrebidCacheVAST struct { ReturnCreative *bool `json:"returnCreative,omitempty"` } +// ExtRequestPrebidBidAdjustments defines the contract for bidrequest.ext.prebid.bidadjustments +type ExtRequestPrebidBidAdjustments struct { + MediaType MediaType `json:"mediatype,omitempty"` +} + +// AdjustmentsByDealID maps a dealID to a slice of bid adjustments +type AdjustmentsByDealID map[string][]Adjustment + +// MediaType defines contract for bidrequest.ext.prebid.bidadjustments.mediatype +// BidderName will map to a DealID that will map to a slice of bid adjustments +type MediaType struct { + Banner map[BidderName]AdjustmentsByDealID `json:"banner,omitempty"` + VideoInstream map[BidderName]AdjustmentsByDealID `json:"video-instream,omitempty"` + VideoOutstream map[BidderName]AdjustmentsByDealID `json:"video-outstream,omitempty"` + Audio map[BidderName]AdjustmentsByDealID `json:"audio,omitempty"` + Native map[BidderName]AdjustmentsByDealID `json:"native,omitempty"` + WildCard map[BidderName]AdjustmentsByDealID `json:"*,omitempty"` +} + +// Adjustment defines the object that will be present in the slice of bid adjustments found from MediaType map +type Adjustment struct { + Type string `json:"adjtype,omitempty"` + Value float64 `json:"value,omitempty"` + Currency string `json:"currency,omitempty"` +} + // ExtRequestTargeting defines the contract for bidrequest.ext.prebid.targeting type ExtRequestTargeting struct { - PriceGranularity *PriceGranularity `json:"pricegranularity,omitempty"` - IncludeWinners *bool `json:"includewinners,omitempty"` - IncludeBidderKeys *bool `json:"includebidderkeys,omitempty"` - IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory,omitempty"` - IncludeFormat bool `json:"includeformat,omitempty"` - DurationRangeSec []int `json:"durationrangesec,omitempty"` - PreferDeals bool `json:"preferdeals,omitempty"` - AppendBidderNames bool `json:"appendbiddernames,omitempty"` + PriceGranularity *PriceGranularity `json:"pricegranularity,omitempty"` + MediaTypePriceGranularity MediaTypePriceGranularity `json:"mediatypepricegranularity,omitempty"` + IncludeWinners *bool `json:"includewinners,omitempty"` + IncludeBidderKeys *bool `json:"includebidderkeys,omitempty"` + IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory,omitempty"` + IncludeFormat bool `json:"includeformat,omitempty"` + DurationRangeSec []int `json:"durationrangesec,omitempty"` + PreferDeals bool `json:"preferdeals,omitempty"` + AppendBidderNames bool `json:"appendbiddernames,omitempty"` } type ExtIncludeBrandCategory struct { @@ -168,7 +207,15 @@ type ExtIncludeBrandCategory struct { TranslateCategories *bool `json:"translatecategories,omitempty"` } +// MediaTypePriceGranularity specify price granularity configuration at the bid type level +type MediaTypePriceGranularity struct { + Banner *PriceGranularity `json:"banner,omitempty"` + Video *PriceGranularity `json:"video,omitempty"` + Native *PriceGranularity `json:"native,omitempty"` +} + // PriceGranularity defines the allowed values for bidrequest.ext.prebid.targeting.pricegranularity +// or bidrequest.ext.prebid.targeting.mediatypepricegranularity.banner|video|native type PriceGranularity struct { Precision *int `json:"precision,omitempty"` Ranges []GranularityRange `json:"ranges,omitempty"` @@ -300,6 +347,16 @@ type ExtRequestPrebidDataEidPermission struct { Bidders []string `json:"bidders"` } +type ExtRequestSdk struct { + Renderers []ExtRequestSdkRenderer `json:"renderers,omitempty"` +} + +type ExtRequestSdkRenderer struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + type ExtMultiBid struct { Bidder string `json:"bidder,omitempty"` Bidders []string `json:"bidders,omitempty"` @@ -314,3 +371,192 @@ func (m ExtMultiBid) String() string { } return fmt.Sprintf("{Bidder:%s, Bidders:%v, MaxBids:%s, TargetBidderCodePrefix:%s}", m.Bidder, m.Bidders, maxBid, m.TargetBidderCodePrefix) } + +func (erp *ExtRequestPrebid) Clone() *ExtRequestPrebid { + if erp == nil { + return nil + } + + clone := *erp + clone.Aliases = maputil.Clone(erp.Aliases) + clone.AliasGVLIDs = maputil.Clone(erp.AliasGVLIDs) + clone.BidAdjustmentFactors = maputil.Clone(erp.BidAdjustmentFactors) + + if erp.BidderConfigs != nil { + clone.BidderConfigs = make([]BidderConfig, len(erp.BidderConfigs)) + for i, bc := range erp.BidderConfigs { + clonedBidderConfig := BidderConfig{Bidders: sliceutil.Clone(bc.Bidders)} + if bc.Config != nil { + config := &Config{ORTB2: ptrutil.Clone(bc.Config.ORTB2)} + clonedBidderConfig.Config = config + } + clone.BidderConfigs[i] = clonedBidderConfig + } + } + + if erp.Cache != nil { + clone.Cache = &ExtRequestPrebidCache{} + if erp.Cache.Bids != nil { + clone.Cache.Bids = &ExtRequestPrebidCacheBids{} + clone.Cache.Bids.ReturnCreative = ptrutil.Clone(erp.Cache.Bids.ReturnCreative) + } + if erp.Cache.VastXML != nil { + clone.Cache.VastXML = &ExtRequestPrebidCacheVAST{} + clone.Cache.VastXML.ReturnCreative = ptrutil.Clone(erp.Cache.VastXML.ReturnCreative) + } + } + + if erp.Channel != nil { + channel := *erp.Channel + clone.Channel = &channel + } + + if erp.CurrencyConversions != nil { + newConvRates := make(map[string]map[string]float64, len(erp.CurrencyConversions.ConversionRates)) + for key, val := range erp.CurrencyConversions.ConversionRates { + newConvRates[key] = maputil.Clone(val) + } + clone.CurrencyConversions = &ExtRequestCurrency{ConversionRates: newConvRates} + if erp.CurrencyConversions.UsePBSRates != nil { + clone.CurrencyConversions.UsePBSRates = ptrutil.ToPtr(*erp.CurrencyConversions.UsePBSRates) + } + } + + if erp.Data != nil { + clone.Data = &ExtRequestPrebidData{Bidders: sliceutil.Clone(erp.Data.Bidders)} + if erp.Data.EidPermissions != nil { + newEidPermissions := make([]ExtRequestPrebidDataEidPermission, len(erp.Data.EidPermissions)) + for i, eidp := range erp.Data.EidPermissions { + newEidPermissions[i] = ExtRequestPrebidDataEidPermission{ + Source: eidp.Source, + Bidders: sliceutil.Clone(eidp.Bidders), + } + } + clone.Data.EidPermissions = newEidPermissions + } + } + + if erp.Experiment != nil { + clone.Experiment = &Experiment{} + if erp.Experiment.AdsCert != nil { + clone.Experiment.AdsCert = ptrutil.ToPtr(*erp.Experiment.AdsCert) + } + } + + if erp.MultiBid != nil { + clone.MultiBid = make([]*ExtMultiBid, len(erp.MultiBid)) + for i, mulBid := range erp.MultiBid { + newMulBid := &ExtMultiBid{ + Bidder: mulBid.Bidder, + Bidders: sliceutil.Clone(mulBid.Bidders), + TargetBidderCodePrefix: mulBid.TargetBidderCodePrefix, + } + if mulBid.MaxBids != nil { + newMulBid.MaxBids = ptrutil.ToPtr(*mulBid.MaxBids) + } + clone.MultiBid[i] = newMulBid + } + } + + if erp.SChains != nil { + clone.SChains = make([]*ExtRequestPrebidSChain, len(erp.SChains)) + for i, schain := range erp.SChains { + newChain := *schain + newNodes := sliceutil.Clone(schain.SChain.Nodes) + for j, node := range newNodes { + if node.HP != nil { + newNodes[j].HP = ptrutil.ToPtr(*newNodes[j].HP) + } + } + newChain.SChain.Nodes = newNodes + clone.SChains[i] = &newChain + } + } + + clone.Server = ptrutil.Clone(erp.Server) + + clone.StoredRequest = ptrutil.Clone(erp.StoredRequest) + + if erp.Targeting != nil { + newTargeting := &ExtRequestTargeting{ + IncludeFormat: erp.Targeting.IncludeFormat, + DurationRangeSec: sliceutil.Clone(erp.Targeting.DurationRangeSec), + PreferDeals: erp.Targeting.PreferDeals, + AppendBidderNames: erp.Targeting.AppendBidderNames, + } + if erp.Targeting.PriceGranularity != nil { + newPriceGranularity := &PriceGranularity{ + Ranges: sliceutil.Clone(erp.Targeting.PriceGranularity.Ranges), + } + newPriceGranularity.Precision = ptrutil.Clone(erp.Targeting.PriceGranularity.Precision) + newTargeting.PriceGranularity = newPriceGranularity + } + newTargeting.IncludeWinners = ptrutil.Clone(erp.Targeting.IncludeWinners) + newTargeting.IncludeBidderKeys = ptrutil.Clone(erp.Targeting.IncludeBidderKeys) + if erp.Targeting.IncludeBrandCategory != nil { + newIncludeBrandCategory := *erp.Targeting.IncludeBrandCategory + newIncludeBrandCategory.TranslateCategories = ptrutil.Clone(erp.Targeting.IncludeBrandCategory.TranslateCategories) + newTargeting.IncludeBrandCategory = &newIncludeBrandCategory + } + clone.Targeting = newTargeting + } + + clone.NoSale = sliceutil.Clone(erp.NoSale) + + if erp.AlternateBidderCodes != nil { + newAlternateBidderCodes := ExtAlternateBidderCodes{Enabled: erp.AlternateBidderCodes.Enabled} + if erp.AlternateBidderCodes.Bidders != nil { + newBidders := make(map[string]ExtAdapterAlternateBidderCodes, len(erp.AlternateBidderCodes.Bidders)) + for key, val := range erp.AlternateBidderCodes.Bidders { + newBidders[key] = ExtAdapterAlternateBidderCodes{ + Enabled: val.Enabled, + AllowedBidderCodes: sliceutil.Clone(val.AllowedBidderCodes), + } + } + newAlternateBidderCodes.Bidders = newBidders + } + clone.AlternateBidderCodes = &newAlternateBidderCodes + } + + if erp.Floors != nil { + clonedFloors := *erp.Floors + clonedFloors.Location = ptrutil.Clone(erp.Floors.Location) + if erp.Floors.Data != nil { + clonedData := *erp.Floors.Data + if erp.Floors.Data.ModelGroups != nil { + clonedData.ModelGroups = make([]PriceFloorModelGroup, len(erp.Floors.Data.ModelGroups)) + for i, pfmg := range erp.Floors.Data.ModelGroups { + clonedData.ModelGroups[i] = pfmg + clonedData.ModelGroups[i].ModelWeight = ptrutil.Clone(pfmg.ModelWeight) + clonedData.ModelGroups[i].Schema.Fields = sliceutil.Clone(pfmg.Schema.Fields) + clonedData.ModelGroups[i].Values = maputil.Clone(pfmg.Values) + } + } + clonedFloors.Data = &clonedData + } + if erp.Floors.Enforcement != nil { + clonedFloors.Enforcement = &PriceFloorEnforcement{ + EnforceJS: ptrutil.Clone(erp.Floors.Enforcement.EnforceJS), + EnforcePBS: ptrutil.Clone(erp.Floors.Enforcement.EnforcePBS), + FloorDeals: ptrutil.Clone(erp.Floors.Enforcement.FloorDeals), + BidAdjustment: ptrutil.Clone(erp.Floors.Enforcement.BidAdjustment), + EnforceRate: erp.Floors.Enforcement.EnforceRate, + } + } + clonedFloors.Enabled = ptrutil.Clone(erp.Floors.Enabled) + clonedFloors.Skipped = ptrutil.Clone(erp.Floors.Skipped) + clone.Floors = &clonedFloors + } + if erp.MultiBidMap != nil { + clone.MultiBidMap = make(map[string]ExtMultiBid, len(erp.MultiBidMap)) + for k, v := range erp.MultiBidMap { + // Make v a deep copy of the ExtMultiBid struct + v.Bidders = sliceutil.Clone(v.Bidders) + v.MaxBids = ptrutil.Clone(v.MaxBids) + clone.MultiBidMap[k] = v + } + } + clone.AdServerTargeting = sliceutil.Clone(erp.AdServerTargeting) + + return &clone +} diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 8d1ecee22b3..a05bae3a6bf 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" ) @@ -141,3 +142,628 @@ var validGranularityTests []granularityTestData = []granularityTestData{ }, }, } + +func TestCloneExtRequestPrebid(t *testing.T) { + testCases := []struct { + name string + prebid *ExtRequestPrebid + prebidCopy *ExtRequestPrebid // manual copy of above prebid object to verify against + mutator func(t *testing.T, prebid *ExtRequestPrebid) // function to modify the prebid object + }{ + { + name: "Nil", // Verify the nil case + prebid: nil, + prebidCopy: nil, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) {}, + }, + { + name: "NoMutateTest", + prebid: &ExtRequestPrebid{ + Aliases: map[string]string{"alias1": "bidder1"}, + BidAdjustmentFactors: map[string]float64{"bidder5": 1.2}, + BidderParams: json.RawMessage(`{}`), + Channel: &ExtRequestPrebidChannel{ + Name: "ABC", + Version: "1.0", + }, + Debug: true, + Experiment: &Experiment{ + AdsCert: &AdsCert{ + Enabled: false, + }, + }, + Server: &ExtRequestPrebidServer{ + ExternalUrl: "http://www.example.com", + GvlID: 5, + DataCenter: "Universe Central", + }, + SupportDeals: true, + }, + prebidCopy: &ExtRequestPrebid{ + Aliases: map[string]string{"alias1": "bidder1"}, + BidAdjustmentFactors: map[string]float64{"bidder5": 1.2}, + BidderParams: json.RawMessage(`{}`), + Channel: &ExtRequestPrebidChannel{ + Name: "ABC", + Version: "1.0", + }, + Debug: true, + Experiment: &Experiment{ + AdsCert: &AdsCert{ + Enabled: false, + }, + }, + Server: &ExtRequestPrebidServer{ + ExternalUrl: "http://www.example.com", + GvlID: 5, + DataCenter: "Universe Central", + }, + SupportDeals: true, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) {}, + }, + { + name: "GeneralTest", + prebid: &ExtRequestPrebid{ + Aliases: map[string]string{"alias1": "bidder1"}, + BidAdjustmentFactors: map[string]float64{"bidder5": 1.2}, + BidderParams: json.RawMessage(`{}`), + Channel: &ExtRequestPrebidChannel{ + Name: "ABC", + Version: "1.0", + }, + Debug: true, + Experiment: &Experiment{ + AdsCert: &AdsCert{ + Enabled: false, + }, + }, + Server: &ExtRequestPrebidServer{ + ExternalUrl: "http://www.example.com", + GvlID: 5, + DataCenter: "Universe Central", + }, + SupportDeals: true, + }, + prebidCopy: &ExtRequestPrebid{ + Aliases: map[string]string{"alias1": "bidder1"}, + BidAdjustmentFactors: map[string]float64{"bidder5": 1.2}, + BidderParams: json.RawMessage(`{}`), + Channel: &ExtRequestPrebidChannel{ + Name: "ABC", + Version: "1.0", + }, + Debug: true, + Experiment: &Experiment{ + AdsCert: &AdsCert{ + Enabled: false, + }, + }, + Server: &ExtRequestPrebidServer{ + ExternalUrl: "http://www.example.com", + GvlID: 5, + DataCenter: "Universe Central", + }, + SupportDeals: true, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.Aliases["alias2"] = "bidder52" + prebid.Aliases["alias1"] = "some other" + prebid.AliasGVLIDs = map[string]uint16{"alias2": 42} + prebid.BidAdjustmentFactors["alias2"] = 1.1 + delete(prebid.BidAdjustmentFactors, "bidder5") + prebid.BidderParams = json.RawMessage(`{"someJSON": true}`) + prebid.Channel = nil + prebid.Data = &ExtRequestPrebidData{ + EidPermissions: []ExtRequestPrebidDataEidPermission{{Source: "mySource", Bidders: []string{"sauceBidder"}}}, + } + prebid.Events = json.RawMessage(`{}`) + prebid.Server.GvlID = 7 + prebid.SupportDeals = false + }, + }, + { + name: "BidderConfig", + prebid: &ExtRequestPrebid{ + BidderConfigs: []BidderConfig{ + { + Bidders: []string{"Bidder1", "bidder2"}, + Config: &Config{&ORTB2{Site: json.RawMessage(`{"value":"config1"}`)}}, + }, + { + Bidders: []string{"Bidder5", "bidder17"}, + Config: &Config{&ORTB2{App: json.RawMessage(`{"value":"config2"}`)}}, + }, + { + Bidders: []string{"foo"}, + Config: &Config{&ORTB2{User: json.RawMessage(`{"value":"config3"}`)}}, + }, + }, + }, + prebidCopy: &ExtRequestPrebid{ + BidderConfigs: []BidderConfig{ + { + Bidders: []string{"Bidder1", "bidder2"}, + Config: &Config{&ORTB2{Site: json.RawMessage(`{"value":"config1"}`)}}, + }, + { + Bidders: []string{"Bidder5", "bidder17"}, + Config: &Config{&ORTB2{App: json.RawMessage(`{"value":"config2"}`)}}, + }, + { + Bidders: []string{"foo"}, + Config: &Config{&ORTB2{User: json.RawMessage(`{"value":"config3"}`)}}, + }, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.BidderConfigs[0].Bidders = append(prebid.BidderConfigs[0].Bidders, "bidder4") + prebid.BidderConfigs[1] = BidderConfig{ + Bidders: []string{"george"}, + Config: &Config{nil}, + } + prebid.BidderConfigs[2].Config.ORTB2.User = json.RawMessage(`{"id": 345}`) + prebid.BidderConfigs = append(prebid.BidderConfigs, BidderConfig{ + Bidders: []string{"bidder2"}, + Config: &Config{&ORTB2{}}, + }) + }, + }, + { + name: "Cache", + prebid: &ExtRequestPrebid{ + Cache: &ExtRequestPrebidCache{ + Bids: &ExtRequestPrebidCacheBids{ + ReturnCreative: ptrutil.ToPtr(true), + }, + VastXML: &ExtRequestPrebidCacheVAST{ + ReturnCreative: ptrutil.ToPtr(false), + }, + }, + }, + prebidCopy: &ExtRequestPrebid{ + Cache: &ExtRequestPrebidCache{ + Bids: &ExtRequestPrebidCacheBids{ + ReturnCreative: ptrutil.ToPtr(true), + }, + VastXML: &ExtRequestPrebidCacheVAST{ + ReturnCreative: ptrutil.ToPtr(false), + }, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.Cache.Bids.ReturnCreative = ptrutil.ToPtr(false) + prebid.Cache.Bids = nil + prebid.Cache.VastXML = &ExtRequestPrebidCacheVAST{ + ReturnCreative: ptrutil.ToPtr(true), + } + }, + }, + { + name: "Currency", + prebid: &ExtRequestPrebid{ + CurrencyConversions: &ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{"A": {"X": 5.4}}, + UsePBSRates: ptrutil.ToPtr(false), + }, + }, + prebidCopy: &ExtRequestPrebid{ + CurrencyConversions: &ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{"A": {"X": 5.4}}, + UsePBSRates: ptrutil.ToPtr(false), + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.CurrencyConversions.ConversionRates["A"]["X"] = 3.4 + prebid.CurrencyConversions.ConversionRates["B"] = make(map[string]float64) + prebid.CurrencyConversions.ConversionRates["B"]["Y"] = 0.76 + prebid.CurrencyConversions.UsePBSRates = ptrutil.ToPtr(true) + }, + }, + { + name: "Data", + prebid: &ExtRequestPrebid{ + Data: &ExtRequestPrebidData{ + EidPermissions: []ExtRequestPrebidDataEidPermission{ + { + Source: "Sauce", + Bidders: []string{"G", "H"}, + }, + { + Source: "Black Hole", + Bidders: []string{"Q", "P"}, + }, + }, + Bidders: []string{"A", "B", "C"}, + }, + }, + prebidCopy: &ExtRequestPrebid{ + Data: &ExtRequestPrebidData{ + EidPermissions: []ExtRequestPrebidDataEidPermission{ + { + Source: "Sauce", + Bidders: []string{"G", "H"}, + }, + { + Source: "Black Hole", + Bidders: []string{"Q", "P"}, + }, + }, + Bidders: []string{"A", "B", "C"}, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.Data.EidPermissions[0].Source = "Fuzzy Bunnies" + prebid.Data.EidPermissions[1].Bidders[0] = "X" + prebid.Data.EidPermissions[0].Bidders = append(prebid.Data.EidPermissions[0].Bidders, "R") + prebid.Data.EidPermissions = append(prebid.Data.EidPermissions, ExtRequestPrebidDataEidPermission{Source: "Harry"}) + prebid.Data.Bidders[1] = "D" + prebid.Data.Bidders = append(prebid.Data.Bidders, "E") + }, + }, + { + name: "Multibid", + prebid: &ExtRequestPrebid{ + MultiBid: []*ExtMultiBid{ + {Bidder: "somebidder", MaxBids: ptrutil.ToPtr(3), TargetBidderCodePrefix: "SB"}, + {Bidders: []string{"A", "B", "C"}, MaxBids: ptrutil.ToPtr(4)}, + }, + }, + prebidCopy: &ExtRequestPrebid{ + MultiBid: []*ExtMultiBid{ + {Bidder: "somebidder", MaxBids: ptrutil.ToPtr(3), TargetBidderCodePrefix: "SB"}, + {Bidders: []string{"A", "B", "C"}, MaxBids: ptrutil.ToPtr(4)}, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.MultiBid[0].MaxBids = ptrutil.ToPtr(2) + prebid.MultiBid[1].Bidders = []string{"C", "D", "E", "F"} + prebid.MultiBid = []*ExtMultiBid{ + {Bidder: "otherbid"}, + } + }, + }, + { + name: "PassthroughSChains", + prebid: &ExtRequestPrebid{ + SChains: []*ExtRequestPrebidSChain{ + { + Bidders: []string{"A", "B", "C"}, + SChain: openrtb2.SupplyChain{ + Complete: 1, + Ver: "2.2", + Ext: json.RawMessage(`{"foo": "bar"}`), + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "something", + Domain: "example.com", + HP: openrtb2.Int8Ptr(1), + }, + }, + }, + }, + }, + }, + prebidCopy: &ExtRequestPrebid{ + SChains: []*ExtRequestPrebidSChain{ + { + Bidders: []string{"A", "B", "C"}, + SChain: openrtb2.SupplyChain{ + Complete: 1, + Ver: "2.2", + Ext: json.RawMessage(`{"foo": "bar"}`), + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "something", + Domain: "example.com", + HP: openrtb2.Int8Ptr(1), + }, + }, + }, + }, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.Passthrough = json.RawMessage(`{"bar": "foo"}`) + prebid.SChains[0].Bidders = append(prebid.SChains[0].Bidders, "D") + prebid.SChains[0].SChain.Ver = "2.3" + prebid.SChains[0].SChain.Complete = 0 + prebid.SChains[0].SChain.Nodes[0].Name = "Alice" + prebid.SChains[0].SChain.Nodes[0].ASI = "New ASI" + prebid.SChains[0].SChain.Nodes[0].HP = openrtb2.Int8Ptr(0) + prebid.SChains[0].SChain.Nodes = append(prebid.SChains[0].SChain.Nodes, prebid.SChains[0].SChain.Nodes[0]) + prebid.SChains = append(prebid.SChains, prebid.SChains[0]) + }, + }, + { + name: "StoredRequest", + prebid: &ExtRequestPrebid{ + StoredRequest: &ExtStoredRequest{ + ID: "abc123", + }, + }, + prebidCopy: &ExtRequestPrebid{ + StoredRequest: &ExtStoredRequest{ + ID: "abc123", + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.StoredRequest.ID = "nada" + prebid.StoredRequest = &ExtStoredRequest{ID: "ID"} + }, + }, + { + name: "Targeting", + prebid: &ExtRequestPrebid{ + Targeting: &ExtRequestTargeting{ + PriceGranularity: &PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []GranularityRange{ + {Max: 2.0, Increment: 0.1}, + {Max: 10.0, Increment: 0.5}, + {Max: 20.0, Increment: 1.0}, + }, + }, + IncludeWinners: ptrutil.ToPtr(true), + IncludeBrandCategory: &ExtIncludeBrandCategory{ + PrimaryAdServer: 1, + Publisher: "Bob", + TranslateCategories: ptrutil.ToPtr(true), + }, + DurationRangeSec: []int{1, 2, 3}, + }, + }, + prebidCopy: &ExtRequestPrebid{ + Targeting: &ExtRequestTargeting{ + PriceGranularity: &PriceGranularity{ + Precision: ptrutil.ToPtr(2), + Ranges: []GranularityRange{ + {Max: 2.0, Increment: 0.1}, + {Max: 10.0, Increment: 0.5}, + {Max: 20.0, Increment: 1.0}, + }, + }, + IncludeWinners: ptrutil.ToPtr(true), + IncludeBrandCategory: &ExtIncludeBrandCategory{ + PrimaryAdServer: 1, + Publisher: "Bob", + TranslateCategories: ptrutil.ToPtr(true), + }, + DurationRangeSec: []int{1, 2, 3}, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.Targeting.PriceGranularity.Ranges[1].Max = 12 + prebid.Targeting.PriceGranularity.Ranges[1].Min = 2.0 + prebid.Targeting.PriceGranularity.Ranges = append(prebid.Targeting.PriceGranularity.Ranges, GranularityRange{Max: 50, Increment: 2.0}) + prebid.Targeting.IncludeWinners = nil + prebid.Targeting.IncludeBidderKeys = ptrutil.ToPtr(true) + prebid.Targeting.IncludeBrandCategory.TranslateCategories = ptrutil.ToPtr(false) + prebid.Targeting.IncludeBrandCategory = nil + prebid.Targeting.DurationRangeSec[1] = 5 + prebid.Targeting.DurationRangeSec = append(prebid.Targeting.DurationRangeSec, 1) + prebid.Targeting.AppendBidderNames = true + }, + }, + { + name: "NoSale", + prebid: &ExtRequestPrebid{ + NoSale: []string{"A", "B", "C"}, + }, + prebidCopy: &ExtRequestPrebid{ + NoSale: []string{"A", "B", "C"}, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.NoSale[1] = "G" + prebid.NoSale = append(prebid.NoSale, "D") + }, + }, + { + name: "AlternateBidderCodes", + prebid: &ExtRequestPrebid{ + AlternateBidderCodes: &ExtAlternateBidderCodes{ + Enabled: true, + Bidders: map[string]ExtAdapterAlternateBidderCodes{ + "X": {Enabled: true, AllowedBidderCodes: []string{"A", "B", "C"}}, + "Y": {Enabled: false, AllowedBidderCodes: []string{"C", "B", "G"}}, + }, + }, + }, + prebidCopy: &ExtRequestPrebid{ + AlternateBidderCodes: &ExtAlternateBidderCodes{ + Enabled: true, + Bidders: map[string]ExtAdapterAlternateBidderCodes{ + "X": {Enabled: true, AllowedBidderCodes: []string{"A", "B", "C"}}, + "Y": {Enabled: false, AllowedBidderCodes: []string{"C", "B", "G"}}, + }, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + newAABC := prebid.AlternateBidderCodes.Bidders["X"] + newAABC.Enabled = false + newAABC.AllowedBidderCodes[1] = "F" + newAABC.AllowedBidderCodes = append(newAABC.AllowedBidderCodes, "Z") + prebid.AlternateBidderCodes.Bidders["X"] = newAABC + prebid.AlternateBidderCodes.Bidders["Z"] = ExtAdapterAlternateBidderCodes{Enabled: true, AllowedBidderCodes: []string{"G", "Z"}} + prebid.AlternateBidderCodes.Enabled = false + prebid.AlternateBidderCodes = nil + }, + }, + { + name: "Floors", + prebid: &ExtRequestPrebid{ + Floors: &PriceFloorRules{ + FloorMin: 0.25, + FloorMinCur: "EUR", + Location: &PriceFloorEndpoint{ + URL: "http://www.example.com", + }, + Data: &PriceFloorData{ + Currency: "USD", + SkipRate: 3, + ModelGroups: []PriceFloorModelGroup{ + { + Currency: "USD", + ModelWeight: ptrutil.ToPtr(0), + SkipRate: 2, + Schema: PriceFloorSchema{ + Fields: []string{"A", "B"}, + Delimiter: "^", + }, + Values: map[string]float64{"A": 2, "B": 1}, + }, + }, + }, + Enforcement: &PriceFloorEnforcement{ + EnforceJS: ptrutil.ToPtr(true), + FloorDeals: ptrutil.ToPtr(false), + EnforceRate: 5, + }, + Enabled: ptrutil.ToPtr(true), + FloorProvider: "Someone", + }, + }, + prebidCopy: &ExtRequestPrebid{ + Floors: &PriceFloorRules{ + FloorMin: 0.25, + FloorMinCur: "EUR", + Location: &PriceFloorEndpoint{ + URL: "http://www.example.com", + }, + Data: &PriceFloorData{ + Currency: "USD", + SkipRate: 3, + ModelGroups: []PriceFloorModelGroup{ + { + Currency: "USD", + ModelWeight: ptrutil.ToPtr(0), + SkipRate: 2, + Schema: PriceFloorSchema{ + Fields: []string{"A", "B"}, + Delimiter: "^", + }, + Values: map[string]float64{"A": 2, "B": 1}, + }, + }, + }, + Enforcement: &PriceFloorEnforcement{ + EnforceJS: ptrutil.ToPtr(true), + FloorDeals: ptrutil.ToPtr(false), + EnforceRate: 5, + }, + Enabled: ptrutil.ToPtr(true), + FloorProvider: "Someone", + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.Floors.Data.ModelGroups[0].Schema.Fields[1] = "C" + prebid.Floors.Data.ModelGroups[0].Schema.Fields = append(prebid.Floors.Data.ModelGroups[0].Schema.Fields, "D") + prebid.Floors.Data.ModelGroups[0].Schema.Delimiter = "," + prebid.Floors.Data.ModelGroups[0].Currency = "CRO" + prebid.Floors.Data.ModelGroups[0].ModelWeight = ptrutil.ToPtr(8) + prebid.Floors.Data.ModelGroups[0].Values["A"] = 0 + prebid.Floors.Data.ModelGroups[0].Values["C"] = 7 + prebid.Floors.Data.ModelGroups = append(prebid.Floors.Data.ModelGroups, PriceFloorModelGroup{}) + prebid.Floors.FloorMin = 0.3 + prebid.Floors.FetchStatus = "arf" + prebid.Floors.Location.URL = "www.nothere.com" + prebid.Floors.Location = nil + prebid.Floors.Enabled = nil + prebid.Floors.Skipped = ptrutil.ToPtr(true) + prebid.Floors.Enforcement.BidAdjustment = ptrutil.ToPtr(true) + prebid.Floors.Enforcement.EnforceJS = ptrutil.ToPtr(false) + prebid.Floors.Enforcement.FloorDeals = nil + prebid.Floors.FloorProvider = "" + }, + }, + { + name: "MultiBidMap", + prebid: &ExtRequestPrebid{ + MultiBidMap: map[string]ExtMultiBid{ + "A": { + Bidder: "J", + Bidders: []string{"X", "Y", "Z"}, + MaxBids: ptrutil.ToPtr(5), + TargetBidderCodePrefix: ">>", + }, + "B": { + Bidder: "J", + Bidders: []string{"One", "Two", "Three"}, + }, + }, + }, + prebidCopy: &ExtRequestPrebid{ + MultiBidMap: map[string]ExtMultiBid{ + "A": { + Bidder: "J", + Bidders: []string{"X", "Y", "Z"}, + MaxBids: ptrutil.ToPtr(5), + TargetBidderCodePrefix: ">>", + }, + "B": { + Bidder: "J", + Bidders: []string{"One", "Two", "Three"}, + }, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + mulbidB := prebid.MultiBidMap["B"] + mulbidB.TargetBidderCodePrefix = "|" + mulbidB.Bidders[1] = "Five" + mulbidB.Bidders = append(mulbidB.Bidders, "Six") + mulbidB.MaxBids = ptrutil.ToPtr(2) + prebid.MultiBidMap["B"] = mulbidB + prebid.MultiBidMap["C"] = ExtMultiBid{Bidder: "alpha", MaxBids: ptrutil.ToPtr(3)} + }, + }, + { + name: "MultiBidMap", + prebid: &ExtRequestPrebid{ + AdServerTargeting: []AdServerTarget{ + { + Key: "A", + Source: "Sauce", + Value: "Gold", + }, + { + Key: "B", + Source: "Omega", + Value: "Dirt", + }, + }, + }, + prebidCopy: &ExtRequestPrebid{ + AdServerTargeting: []AdServerTarget{ + { + Key: "A", + Source: "Sauce", + Value: "Gold", + }, + { + Key: "B", + Source: "Omega", + Value: "Dirt", + }, + }, + }, + mutator: func(t *testing.T, prebid *ExtRequestPrebid) { + prebid.AdServerTargeting[0].Key = "Five" + prebid.AdServerTargeting[1].Value = "Dust" + prebid.AdServerTargeting = append(prebid.AdServerTargeting, AdServerTarget{Key: "Val"}) + prebid.AdServerTargeting = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.prebid.Clone() + if test.prebid != nil { + assert.NotSame(t, test.prebid, clone) + } + test.mutator(t, test.prebid) + assert.Equal(t, test.prebidCopy, clone) + }) + } + +} diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index 8fe33fff4f2..5f5636d602a 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -5,6 +5,9 @@ import ( "errors" "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/maputil" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/prebid/prebid-server/util/sliceutil" ) // RequestWrapper wraps the OpenRTB request to provide a storage location for unmarshalled ext fields, so they @@ -351,6 +354,27 @@ func (rw *RequestWrapper) rebuildSourceExt() error { return nil } +func (rw *RequestWrapper) Clone() *RequestWrapper { + if rw == nil { + return nil + } + clone := *rw + newImpWrappers := make([]*ImpWrapper, len(rw.impWrappers)) + for i, iw := range rw.impWrappers { + newImpWrappers[i] = iw.Clone() + } + clone.impWrappers = newImpWrappers + clone.userExt = rw.userExt.Clone() + clone.deviceExt = rw.deviceExt.Clone() + clone.requestExt = rw.requestExt.Clone() + clone.appExt = rw.appExt.Clone() + clone.regExt = rw.regExt.Clone() + clone.siteExt = rw.siteExt.Clone() + clone.sourceExt = rw.sourceExt.Clone() + + return &clone +} + // --------------------------------------------------------------- // UserExt provides an interface for request.user.ext // --------------------------------------------------------------- @@ -606,6 +630,43 @@ func (ue *UserExt) SetEid(eid *[]openrtb2.EID) { ue.eidsDirty = true } +func (ue *UserExt) Clone() *UserExt { + if ue == nil { + return nil + } + clone := *ue + clone.ext = maputil.Clone(ue.ext) + + if ue.consent != nil { + clonedConsent := *ue.consent + clone.consent = &clonedConsent + } + + if ue.prebid != nil { + clone.prebid = &ExtUserPrebid{} + clone.prebid.BuyerUIDs = maputil.Clone(ue.prebid.BuyerUIDs) + } + + if ue.eids != nil { + clonedEids := make([]openrtb2.EID, len(*ue.eids)) + for i, eid := range *ue.eids { + newEid := eid + newEid.UIDs = sliceutil.Clone(eid.UIDs) + clonedEids[i] = newEid + } + clone.eids = &clonedEids + } + + if ue.consentedProvidersSettingsIn != nil { + clone.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{ConsentedProvidersString: ue.consentedProvidersSettingsIn.ConsentedProvidersString} + } + if ue.consentedProvidersSettingsOut != nil { + clone.consentedProvidersSettingsOut = &ConsentedProvidersSettingsOut{ConsentedProvidersList: sliceutil.Clone(ue.consentedProvidersSettingsOut.ConsentedProvidersList)} + } + + return &clone +} + // --------------------------------------------------------------- // RequestExt provides an interface for request.ext // --------------------------------------------------------------- @@ -741,6 +802,23 @@ func (re *RequestExt) SetSChain(schain *openrtb2.SupplyChain) { re.schainDirty = true } +func (re *RequestExt) Clone() *RequestExt { + if re == nil { + return nil + } + + clone := *re + clone.ext = maputil.Clone(re.ext) + + if re.prebid != nil { + clone.prebid = re.prebid.Clone() + } + + clone.schain = cloneSupplyChain(re.schain) + + return &clone +} + // --------------------------------------------------------------- // DeviceExt provides an interface for request.device.ext // --------------------------------------------------------------- @@ -837,6 +915,26 @@ func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) { de.prebidDirty = true } +func (de *DeviceExt) Clone() *DeviceExt { + if de == nil { + return nil + } + + clone := *de + clone.ext = maputil.Clone(de.ext) + + if de.prebid != nil { + clonedPrebid := *de.prebid + if clonedPrebid.Interstitial != nil { + clonedInterstitial := *de.prebid.Interstitial + clonedPrebid.Interstitial = &clonedInterstitial + } + clone.prebid = &clonedPrebid + } + + return &clone +} + // --------------------------------------------------------------- // AppExt provides an interface for request.app.ext // --------------------------------------------------------------- @@ -929,6 +1027,19 @@ func (ae *AppExt) SetPrebid(prebid *ExtAppPrebid) { ae.prebidDirty = true } +func (ae *AppExt) Clone() *AppExt { + if ae == nil { + return nil + } + + clone := *ae + clone.ext = maputil.Clone(ae.ext) + + clone.prebid = ptrutil.Clone(ae.prebid) + + return &clone +} + // --------------------------------------------------------------- // RegExt provides an interface for request.regs.ext // --------------------------------------------------------------- @@ -1045,6 +1156,19 @@ func (re *RegExt) SetUSPrivacy(usPrivacy string) { re.usPrivacyDirty = true } +func (re *RegExt) Clone() *RegExt { + if re == nil { + return nil + } + + clone := *re + clone.ext = maputil.Clone(re.ext) + + clone.gdpr = ptrutil.Clone(re.gdpr) + + return &clone +} + // --------------------------------------------------------------- // SiteExt provides an interface for request.site.ext // --------------------------------------------------------------- @@ -1128,6 +1252,18 @@ func (se *SiteExt) SetAmp(amp *int8) { se.ampDirty = true } +func (se *SiteExt) Clone() *SiteExt { + if se == nil { + return nil + } + + clone := *se + clone.ext = maputil.Clone(se.ext) + clone.amp = ptrutil.Clone(se.amp) + + return &clone +} + // --------------------------------------------------------------- // SourceExt provides an interface for request.source.ext // --------------------------------------------------------------- @@ -1219,6 +1355,19 @@ func (se *SourceExt) SetSChain(schain *openrtb2.SupplyChain) { se.schainDirty = true } +func (se *SourceExt) Clone() *SourceExt { + if se == nil { + return nil + } + + clone := *se + clone.ext = maputil.Clone(se.ext) + + clone.schain = cloneSupplyChain(se.schain) + + return &clone +} + // ImpWrapper wraps an OpenRTB impression object to provide storage for unmarshalled ext fields, so they // will not need to be unmarshalled multiple times. It is intended to use the ImpWrapper via the RequestWrapper // and follow the same usage conventions. @@ -1265,6 +1414,17 @@ func (w *ImpWrapper) rebuildImpExt() error { return nil } +func (w *ImpWrapper) Clone() *ImpWrapper { + if w == nil { + return nil + } + + clone := *w + clone.impExt = w.impExt.Clone() + + return &clone +} + // --------------------------------------------------------------- // ImpExt provides an interface for imp.ext // --------------------------------------------------------------- @@ -1429,3 +1589,32 @@ func (e *ImpExt) GetGpId() string { func CreateImpExtForTesting(ext map[string]json.RawMessage, prebid *ExtImpPrebid) ImpExt { return ImpExt{ext: ext, prebid: prebid} } + +func (e *ImpExt) Clone() *ImpExt { + if e == nil { + return nil + } + + clone := *e + clone.ext = maputil.Clone(e.ext) + + if e.prebid != nil { + clonedPrebid := *e.prebid + clonedPrebid.StoredRequest = ptrutil.Clone(e.prebid.StoredRequest) + clonedPrebid.StoredAuctionResponse = ptrutil.Clone(e.prebid.StoredAuctionResponse) + if e.prebid.StoredBidResponse != nil { + clonedPrebid.StoredBidResponse = make([]ExtStoredBidResponse, len(e.prebid.StoredBidResponse)) + for i, sbr := range e.prebid.StoredBidResponse { + clonedPrebid.StoredBidResponse[i] = sbr + clonedPrebid.StoredBidResponse[i].ReplaceImpId = ptrutil.Clone(sbr.ReplaceImpId) + } + } + clonedPrebid.IsRewardedInventory = ptrutil.Clone(e.prebid.IsRewardedInventory) + clonedPrebid.Bidder = maputil.Clone(e.prebid.Bidder) + clonedPrebid.Options = ptrutil.Clone(e.prebid.Options) + clonedPrebid.Floors = ptrutil.Clone(e.prebid.Floors) + clone.prebid = &clonedPrebid + } + + return &clone +} diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index 2986840b0c0..e02dd741519 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -5,9 +5,130 @@ import ( "testing" "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" ) +func TestCloneRequestWrapper(t *testing.T) { + testCases := []struct { + name string + reqWrap *RequestWrapper + reqWrapCopy *RequestWrapper // manual copy of above ext object to verify against + mutator func(t *testing.T, reqWrap *RequestWrapper) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + reqWrap: nil, + reqWrapCopy: nil, + mutator: func(t *testing.T, reqWrap *RequestWrapper) {}, + }, + { + name: "NoMutate", + reqWrap: &RequestWrapper{ + impWrappers: []*ImpWrapper{ + { + impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"}, + }, + { + impExt: &ImpExt{tid: "star"}, + }, + }, + userExt: &UserExt{consentDirty: true}, + deviceExt: &DeviceExt{extDirty: true}, + requestExt: &RequestExt{ + prebid: &ExtRequestPrebid{Integration: "derivative"}, + }, + appExt: &AppExt{prebidDirty: true}, + regExt: &RegExt{usPrivacy: "foo"}, + siteExt: &SiteExt{amp: ptrutil.ToPtr[int8](1)}, + sourceExt: &SourceExt{schainDirty: true}, + }, + reqWrapCopy: &RequestWrapper{ + impWrappers: []*ImpWrapper{ + { + impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"}, + }, + { + impExt: &ImpExt{tid: "star"}, + }, + }, + userExt: &UserExt{consentDirty: true}, + deviceExt: &DeviceExt{extDirty: true}, + requestExt: &RequestExt{ + prebid: &ExtRequestPrebid{Integration: "derivative"}, + }, + appExt: &AppExt{prebidDirty: true}, + regExt: &RegExt{usPrivacy: "foo"}, + siteExt: &SiteExt{amp: ptrutil.ToPtr[int8](1)}, + sourceExt: &SourceExt{schainDirty: true}, + }, + mutator: func(t *testing.T, reqWrap *RequestWrapper) {}, + }, + { + name: "General", + reqWrap: &RequestWrapper{ + impWrappers: []*ImpWrapper{ + { + impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"}, + }, + { + impExt: &ImpExt{tid: "star"}, + }, + }, + userExt: &UserExt{consentDirty: true}, + deviceExt: &DeviceExt{extDirty: true}, + requestExt: &RequestExt{ + prebid: &ExtRequestPrebid{Integration: "derivative"}, + }, + appExt: &AppExt{prebidDirty: true}, + regExt: &RegExt{usPrivacy: "foo"}, + siteExt: &SiteExt{amp: ptrutil.ToPtr[int8](1)}, + sourceExt: &SourceExt{schainDirty: true}, + }, + reqWrapCopy: &RequestWrapper{ + impWrappers: []*ImpWrapper{ + { + impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"}, + }, + { + impExt: &ImpExt{tid: "star"}, + }, + }, + userExt: &UserExt{consentDirty: true}, + deviceExt: &DeviceExt{extDirty: true}, + requestExt: &RequestExt{ + prebid: &ExtRequestPrebid{Integration: "derivative"}, + }, + appExt: &AppExt{prebidDirty: true}, + regExt: &RegExt{usPrivacy: "foo"}, + siteExt: &SiteExt{amp: ptrutil.ToPtr[int8](1)}, + sourceExt: &SourceExt{schainDirty: true}, + }, + mutator: func(t *testing.T, reqWrap *RequestWrapper) { + reqWrap.impWrappers[1].impExt.prebidDirty = true + reqWrap.impWrappers[0] = nil + reqWrap.impWrappers = append(reqWrap.impWrappers, &ImpWrapper{impExt: &ImpExt{tid: "star"}}) + reqWrap.impWrappers = nil + reqWrap.userExt = nil + reqWrap.deviceExt = nil + reqWrap.requestExt = nil + reqWrap.appExt = nil + reqWrap.regExt = nil + reqWrap.siteExt = nil + reqWrap.sourceExt = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.reqWrap.Clone() + test.mutator(t, test.reqWrap) + assert.Equal(t, test.reqWrapCopy, clone) + }) + } +} + func TestUserExt(t *testing.T) { userExt := &UserExt{} @@ -465,6 +586,162 @@ func TestUserExtUnmarshal(t *testing.T) { } } +func TestCloneUserExt(t *testing.T) { + testCases := []struct { + name string + userExt *UserExt + userExtCopy *UserExt // manual copy of above ext object to verify against + mutator func(t *testing.T, userExt *UserExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + userExt: nil, + userExtCopy: nil, + mutator: func(t *testing.T, user *UserExt) {}, + }, + { + name: "NoMutate", + userExt: &UserExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + consent: ptrutil.ToPtr("Myconsent"), + consentDirty: true, + prebid: &ExtUserPrebid{ + BuyerUIDs: map[string]string{"A": "X", "B": "Y"}, + }, + prebidDirty: true, + eids: &[]openrtb2.EID{}, + }, + userExtCopy: &UserExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + consent: ptrutil.ToPtr("Myconsent"), + consentDirty: true, + prebid: &ExtUserPrebid{ + BuyerUIDs: map[string]string{"A": "X", "B": "Y"}, + }, + prebidDirty: true, + eids: &[]openrtb2.EID{}, + }, + mutator: func(t *testing.T, user *UserExt) {}, + }, + { + name: "General", + userExt: &UserExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + consent: ptrutil.ToPtr("Myconsent"), + consentDirty: true, + prebid: &ExtUserPrebid{ + BuyerUIDs: map[string]string{"A": "X", "B": "Y"}, + }, + prebidDirty: true, + eids: &[]openrtb2.EID{}, + }, + userExtCopy: &UserExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + consent: ptrutil.ToPtr("Myconsent"), + consentDirty: true, + prebid: &ExtUserPrebid{ + BuyerUIDs: map[string]string{"A": "X", "B": "Y"}, + }, + prebidDirty: true, + eids: &[]openrtb2.EID{}, + }, + mutator: func(t *testing.T, user *UserExt) { + user.ext["A"] = json.RawMessage(`G`) + user.ext["C"] = json.RawMessage(`L`) + user.extDirty = true + user.consent = nil + user.consentDirty = false + user.prebid.BuyerUIDs["A"] = "C" + user.prebid.BuyerUIDs["C"] = "A" + user.prebid = nil + }, + }, + { + name: "EIDs", + userExt: &UserExt{ + eids: &[]openrtb2.EID{ + { + Source: "Sauce", + UIDs: []openrtb2.UID{ + {ID: "A", AType: 5, Ext: json.RawMessage(`{}`)}, + {ID: "B", AType: 1, Ext: json.RawMessage(`{"extra": "stuff"}`)}, + }, + }, + { + Source: "Moon", + UIDs: []openrtb2.UID{ + {ID: "G", AType: 3, Ext: json.RawMessage(`{}`)}, + {ID: "D", AType: 1}, + }, + }, + }, + }, + userExtCopy: &UserExt{ + eids: &[]openrtb2.EID{ + { + Source: "Sauce", + UIDs: []openrtb2.UID{ + {ID: "A", AType: 5, Ext: json.RawMessage(`{}`)}, + {ID: "B", AType: 1, Ext: json.RawMessage(`{"extra": "stuff"}`)}, + }, + }, + { + Source: "Moon", + UIDs: []openrtb2.UID{ + {ID: "G", AType: 3, Ext: json.RawMessage(`{}`)}, + {ID: "D", AType: 1}, + }, + }, + }, + }, + mutator: func(t *testing.T, userExt *UserExt) { + eids := *userExt.eids + eids[0].UIDs[1].ID = "G2" + eids[1].UIDs[0].AType = 0 + eids[0].UIDs = append(eids[0].UIDs, openrtb2.UID{ID: "Z", AType: 2}) + eids = append(eids, openrtb2.EID{Source: "Blank"}) + userExt.eids = nil + }, + }, + { + name: "ConsentedProviders", + userExt: &UserExt{ + consentedProvidersSettingsIn: &ConsentedProvidersSettingsIn{ + ConsentedProvidersString: "A,B,C", + }, + consentedProvidersSettingsOut: &ConsentedProvidersSettingsOut{ + ConsentedProvidersList: []int{1, 2, 3, 4}, + }, + }, + userExtCopy: &UserExt{ + consentedProvidersSettingsIn: &ConsentedProvidersSettingsIn{ + ConsentedProvidersString: "A,B,C", + }, + consentedProvidersSettingsOut: &ConsentedProvidersSettingsOut{ + ConsentedProvidersList: []int{1, 2, 3, 4}, + }, + }, + mutator: func(t *testing.T, userExt *UserExt) { + userExt.consentedProvidersSettingsIn.ConsentedProvidersString = "B,C,D" + userExt.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{ + ConsentedProvidersString: "G,H,I", + } + userExt.consentedProvidersSettingsOut.ConsentedProvidersList[1] = 5 + userExt.consentedProvidersSettingsOut.ConsentedProvidersList = append(userExt.consentedProvidersSettingsOut.ConsentedProvidersList, 7) + userExt.consentedProvidersSettingsOut = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.userExt.Clone() + test.mutator(t, test.userExt) + assert.Equal(t, test.userExtCopy, clone) + }) + } +} + func TestRebuildDeviceExt(t *testing.T) { prebidContent1 := ExtDevicePrebid{Interstitial: &ExtDeviceInt{MinWidthPerc: 1}} prebidContent2 := ExtDevicePrebid{Interstitial: &ExtDeviceInt{MinWidthPerc: 2}} @@ -611,6 +888,172 @@ func TestRebuildRequestExt(t *testing.T) { } } +func TestCloneRequestExt(t *testing.T) { + testCases := []struct { + name string + reqExt *RequestExt + reqExtCopy *RequestExt // manual copy of above ext object to verify against + mutator func(t *testing.T, reqExt *RequestExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + reqExt: nil, + reqExtCopy: nil, + mutator: func(t *testing.T, reqExt *RequestExt) {}, + }, + { + name: "NoMutate", // Verify the nil case + reqExt: &RequestExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtRequestPrebid{ + BidderParams: json.RawMessage(`{}`), + }, + }, + reqExtCopy: &RequestExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtRequestPrebid{ + BidderParams: json.RawMessage(`{}`), + }, + }, + mutator: func(t *testing.T, reqExt *RequestExt) {}, + }, + { + name: "General", // Verify the nil case + reqExt: &RequestExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtRequestPrebid{ + BidderParams: json.RawMessage(`{}`), + }, + }, + reqExtCopy: &RequestExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtRequestPrebid{ + BidderParams: json.RawMessage(`{}`), + }, + }, + mutator: func(t *testing.T, reqExt *RequestExt) { + reqExt.ext["A"] = json.RawMessage(`"string"`) + reqExt.ext["C"] = json.RawMessage(`{}`) + reqExt.extDirty = false + reqExt.prebid.Channel = &ExtRequestPrebidChannel{Name: "Bob"} + reqExt.prebid.BidderParams = nil + reqExt.prebid = nil + }, + }, + { + name: "SChain", // Verify the nil case + reqExt: &RequestExt{ + schain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.1", + Nodes: []openrtb2.SupplyChainNode{ + {ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)}, + {ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)}, + }, + }, + }, + reqExtCopy: &RequestExt{ + schain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.1", + Nodes: []openrtb2.SupplyChainNode{ + {ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)}, + {ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)}, + }, + }, + }, + mutator: func(t *testing.T, reqExt *RequestExt) { + reqExt.schain.Complete = 0 + reqExt.schain.Ver = "1.2" + reqExt.schain.Nodes[0].ASI = "some" + reqExt.schain.Nodes[1].HP = nil + reqExt.schain.Nodes = append(reqExt.schain.Nodes, openrtb2.SupplyChainNode{ASI: "added"}) + reqExt.schain = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.reqExt.Clone() + test.mutator(t, test.reqExt) + assert.Equal(t, test.reqExtCopy, clone) + }) + } + +} + +func TestCloneDeviceExt(t *testing.T) { + testCases := []struct { + name string + devExt *DeviceExt + devExtCopy *DeviceExt // manual copy of above ext object to verify against + mutator func(t *testing.T, devExt *DeviceExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + devExt: nil, + devExtCopy: nil, + mutator: func(t *testing.T, devExt *DeviceExt) {}, + }, + { + name: "NoMutate", + devExt: &DeviceExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtDevicePrebid{ + Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, + }, + }, + devExtCopy: &DeviceExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtDevicePrebid{ + Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, + }, + }, + mutator: func(t *testing.T, devExt *DeviceExt) {}, + }, + { + name: "General", + devExt: &DeviceExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtDevicePrebid{ + Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, + }, + }, + devExtCopy: &DeviceExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, + extDirty: true, + prebid: &ExtDevicePrebid{ + Interstitial: &ExtDeviceInt{MinWidthPerc: 65, MinHeightPerc: 75}, + }, + }, + mutator: func(t *testing.T, devExt *DeviceExt) { + devExt.ext["A"] = json.RawMessage(`"string"`) + devExt.ext["C"] = json.RawMessage(`{}`) + devExt.extDirty = false + devExt.prebid.Interstitial.MinHeightPerc = 55 + devExt.prebid.Interstitial = &ExtDeviceInt{MinWidthPerc: 80} + devExt.prebid = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.devExt.Clone() + test.mutator(t, test.devExt) + assert.Equal(t, test.devExtCopy, clone) + }) + } +} + func TestRebuildAppExt(t *testing.T) { prebidContent1 := ExtAppPrebid{Source: "1"} prebidContent2 := ExtAppPrebid{Source: "2"} @@ -693,6 +1136,143 @@ func TestRebuildAppExt(t *testing.T) { } } +func TestCloneAppExt(t *testing.T) { + testCases := []struct { + name string + appExt *AppExt + appExtCopy *AppExt // manual copy of above ext object to verify against + mutator func(t *testing.T, appExt *AppExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + appExt: nil, + appExtCopy: nil, + mutator: func(t *testing.T, appExt *AppExt) {}, + }, + { + name: "NoMutate", + appExt: &AppExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + prebid: &ExtAppPrebid{ + Source: "Sauce", + Version: "2.2", + }, + }, + appExtCopy: &AppExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + prebid: &ExtAppPrebid{ + Source: "Sauce", + Version: "2.2", + }, + }, + mutator: func(t *testing.T, appExt *AppExt) {}, + }, + { + name: "General", + appExt: &AppExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + prebid: &ExtAppPrebid{ + Source: "Sauce", + Version: "2.2", + }, + }, + appExtCopy: &AppExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + prebid: &ExtAppPrebid{ + Source: "Sauce", + Version: "2.2", + }, + }, + mutator: func(t *testing.T, appExt *AppExt) { + appExt.ext["A"] = json.RawMessage(`"string"`) + appExt.ext["C"] = json.RawMessage(`{}`) + appExt.extDirty = false + appExt.prebid.Source = "foobar" + appExt.prebid = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.appExt.Clone() + test.mutator(t, test.appExt) + assert.Equal(t, test.appExtCopy, clone) + }) + } +} + +func TestCloneRegExt(t *testing.T) { + testCases := []struct { + name string + regExt *RegExt + regExtCopy *RegExt // manual copy of above ext object to verify against + mutator func(t *testing.T, regExt *RegExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + regExt: nil, + regExtCopy: nil, + mutator: func(t *testing.T, appExt *RegExt) {}, + }, + { + name: "NoMutate", + regExt: &RegExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + gdpr: ptrutil.ToPtr[int8](1), + usPrivacy: "priv", + usPrivacyDirty: true, + }, + regExtCopy: &RegExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + gdpr: ptrutil.ToPtr[int8](1), + usPrivacy: "priv", + usPrivacyDirty: true, + }, + mutator: func(t *testing.T, appExt *RegExt) {}, + }, + { + name: "General", + regExt: &RegExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + gdpr: ptrutil.ToPtr[int8](1), + usPrivacy: "priv", + usPrivacyDirty: true, + }, + regExtCopy: &RegExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + gdpr: ptrutil.ToPtr[int8](1), + usPrivacy: "priv", + usPrivacyDirty: true, + }, + mutator: func(t *testing.T, appExt *RegExt) { + appExt.ext["A"] = json.RawMessage(`"string"`) + appExt.ext["C"] = json.RawMessage(`{}`) + appExt.extDirty = false + appExt.gdpr = nil + appExt.gdprDirty = true + appExt.usPrivacy = "Other" + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.regExt.Clone() + test.mutator(t, test.regExt) + assert.Equal(t, test.regExtCopy, clone) + }) + } +} + func TestRebuildSiteExt(t *testing.T) { int1 := int8(1) int2 := int8(2) @@ -775,6 +1355,64 @@ func TestRebuildSiteExt(t *testing.T) { } } +func TestCloneSiteExt(t *testing.T) { + testCases := []struct { + name string + siteExt *SiteExt + siteExtCopy *SiteExt // manual copy of above ext object to verify against + mutator func(t *testing.T, siteExt *SiteExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + siteExt: nil, + siteExtCopy: nil, + mutator: func(t *testing.T, siteExt *SiteExt) {}, + }, + { + name: "NoMutate", + siteExt: &SiteExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + amp: ptrutil.ToPtr[int8](1), + }, + siteExtCopy: &SiteExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + amp: ptrutil.ToPtr[int8](1), + }, + mutator: func(t *testing.T, siteExt *SiteExt) {}, + }, + { + name: "General", + siteExt: &SiteExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + amp: ptrutil.ToPtr[int8](1), + }, + siteExtCopy: &SiteExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + amp: ptrutil.ToPtr[int8](1), + }, + mutator: func(t *testing.T, siteExt *SiteExt) { + siteExt.ext["A"] = json.RawMessage(`"string"`) + siteExt.ext["C"] = json.RawMessage(`{}`) + siteExt.extDirty = false + siteExt.amp = nil + siteExt.ampDirty = true + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.siteExt.Clone() + test.mutator(t, test.siteExt) + assert.Equal(t, test.siteExtCopy, clone) + }) + } +} + func TestRebuildSourceExt(t *testing.T) { schainContent1 := openrtb2.SupplyChain{Ver: "1"} schainContent2 := openrtb2.SupplyChain{Ver: "2"} @@ -857,6 +1495,69 @@ func TestRebuildSourceExt(t *testing.T) { } } +func TestCloneSourceExt(t *testing.T) { + testCases := []struct { + name string + sourceExt *SourceExt + sourceExtCopy *SourceExt // manual copy of above ext object to verify against + mutator func(t *testing.T, sourceExt *SourceExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + sourceExt: nil, + sourceExtCopy: nil, + mutator: func(t *testing.T, sourceExt *SourceExt) {}, + }, + { + name: "NoMutate", + sourceExt: &SourceExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + schain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.1", + Nodes: []openrtb2.SupplyChainNode{ + {ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)}, + {ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)}, + }, + }, + }, + sourceExtCopy: &SourceExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + schain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.1", + Nodes: []openrtb2.SupplyChainNode{ + {ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)}, + {ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)}, + }, + }, + }, + mutator: func(t *testing.T, sourceExt *SourceExt) { + sourceExt.ext["A"] = json.RawMessage(`"string"`) + sourceExt.ext["C"] = json.RawMessage(`{}`) + sourceExt.extDirty = false + sourceExt.schain.Complete = 0 + sourceExt.schain.Ver = "1.2" + sourceExt.schain.Nodes[0].ASI = "some" + sourceExt.schain.Nodes[1].HP = nil + sourceExt.schain.Nodes = append(sourceExt.schain.Nodes, openrtb2.SupplyChainNode{ASI: "added"}) + sourceExt.schain = nil + + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.sourceExt.Clone() + test.mutator(t, test.sourceExt) + assert.Equal(t, test.sourceExtCopy, clone) + }) + } +} + func TestImpWrapperRebuildImp(t *testing.T) { var ( isRewardedInventoryOne int8 = 1 @@ -1026,3 +1727,184 @@ func TestImpExtTid(t *testing.T) { assert.Equal(t, "tid", impExt.GetTid(), "ImpExt tid is incorrect") assert.Equal(t, true, impExt.Dirty(), "New impext should be dirty.") } + +func TestCloneImpWrapper(t *testing.T) { + testCases := []struct { + name string + impWrapper *ImpWrapper + impWrapperCopy *ImpWrapper // manual copy of above ext object to verify against + mutator func(t *testing.T, impWrapper *ImpWrapper) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + impWrapper: nil, + impWrapperCopy: nil, + mutator: func(t *testing.T, impWrapper *ImpWrapper) {}, + }, + { + name: "NoMutate", + impWrapper: &ImpWrapper{ + impExt: &ImpExt{ + tid: "occupied", + }, + }, + impWrapperCopy: &ImpWrapper{ + impExt: &ImpExt{ + tid: "occupied", + }, + }, + mutator: func(t *testing.T, impWrapper *ImpWrapper) {}, + }, + { + name: "General", + impWrapper: &ImpWrapper{ + impExt: &ImpExt{ + tid: "occupied", + }, + }, + impWrapperCopy: &ImpWrapper{ + impExt: &ImpExt{ + tid: "occupied", + }, + }, + mutator: func(t *testing.T, impWrapper *ImpWrapper) { + impWrapper.impExt.extDirty = true + impWrapper.impExt.tid = "Something" + impWrapper.impExt = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.impWrapper.Clone() + test.mutator(t, test.impWrapper) + assert.Equal(t, test.impWrapperCopy, clone) + }) + } +} + +func TestCloneImpExt(t *testing.T) { + testCases := []struct { + name string + impExt *ImpExt + impExtCopy *ImpExt // manual copy of above ext object to verify against + mutator func(t *testing.T, impExt *ImpExt) // function to modify the Ext object + }{ + { + name: "Nil", // Verify the nil case + impExt: nil, + impExtCopy: nil, + mutator: func(t *testing.T, impExt *ImpExt) {}, + }, + { + name: "NoMutate", + impExt: &ImpExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + tid: "TID", + }, + impExtCopy: &ImpExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + tid: "TID", + }, + mutator: func(t *testing.T, impExt *ImpExt) {}, + }, + { + name: "General", + impExt: &ImpExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + tid: "TID", + }, + impExtCopy: &ImpExt{ + ext: map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)}, + extDirty: true, + tid: "TID", + }, + mutator: func(t *testing.T, impExt *ImpExt) { + impExt.ext["A"] = json.RawMessage(`"string"`) + impExt.ext["C"] = json.RawMessage(`{}`) + impExt.extDirty = false + impExt.tid = "other" + impExt.tidDirty = true + }, + }, + { + name: "Prebid", + impExt: &ImpExt{ + prebid: &ExtImpPrebid{ + StoredRequest: &ExtStoredRequest{ID: "abc123"}, + StoredAuctionResponse: &ExtStoredAuctionResponse{ID: "123abc"}, + StoredBidResponse: []ExtStoredBidResponse{ + {ID: "foo", Bidder: "bar", ReplaceImpId: ptrutil.ToPtr(true)}, + {ID: "def", Bidder: "xyz", ReplaceImpId: ptrutil.ToPtr(false)}, + }, + IsRewardedInventory: ptrutil.ToPtr[int8](1), + Bidder: map[string]json.RawMessage{ + "abc": json.RawMessage(`{}`), + "def": json.RawMessage(`{"alpha":"beta"}`), + }, + Options: &Options{EchoVideoAttrs: true}, + Passthrough: json.RawMessage(`{"foo":"bar"}`), + Floors: &ExtImpPrebidFloors{ + FloorRule: "Rule 16", + FloorRuleValue: 16.17, + FloorValue: 6.7, + }, + }, + }, + impExtCopy: &ImpExt{ + prebid: &ExtImpPrebid{ + StoredRequest: &ExtStoredRequest{ID: "abc123"}, + StoredAuctionResponse: &ExtStoredAuctionResponse{ID: "123abc"}, + StoredBidResponse: []ExtStoredBidResponse{ + {ID: "foo", Bidder: "bar", ReplaceImpId: ptrutil.ToPtr(true)}, + {ID: "def", Bidder: "xyz", ReplaceImpId: ptrutil.ToPtr(false)}, + }, + IsRewardedInventory: ptrutil.ToPtr[int8](1), + Bidder: map[string]json.RawMessage{ + "abc": json.RawMessage(`{}`), + "def": json.RawMessage(`{"alpha":"beta"}`), + }, + Options: &Options{EchoVideoAttrs: true}, + Passthrough: json.RawMessage(`{"foo":"bar"}`), + Floors: &ExtImpPrebidFloors{ + FloorRule: "Rule 16", + FloorRuleValue: 16.17, + FloorValue: 6.7, + }, + }, + }, + mutator: func(t *testing.T, impExt *ImpExt) { + impExt.prebid.StoredRequest.ID = "seventy" + impExt.prebid.StoredRequest = nil + impExt.prebid.StoredAuctionResponse.ID = "xyz" + impExt.prebid.StoredAuctionResponse = nil + impExt.prebid.StoredBidResponse[0].ID = "alpha" + impExt.prebid.StoredBidResponse[1].ReplaceImpId = nil + impExt.prebid.StoredBidResponse[0] = ExtStoredBidResponse{ID: "o", Bidder: "k", ReplaceImpId: ptrutil.ToPtr(false)} + impExt.prebid.StoredBidResponse = append(impExt.prebid.StoredBidResponse, ExtStoredBidResponse{ID: "jay", Bidder: "walk"}) + impExt.prebid.IsRewardedInventory = nil + impExt.prebid.Bidder["def"] = json.RawMessage(``) + delete(impExt.prebid.Bidder, "abc") + impExt.prebid.Bidder["xyz"] = json.RawMessage(`{"jar":5}`) + impExt.prebid.Options.EchoVideoAttrs = false + impExt.prebid.Options = nil + impExt.prebid.Passthrough = json.RawMessage(`{}`) + impExt.prebid.Floors.FloorRule = "Friday" + impExt.prebid.Floors.FloorMinCur = "EUR" + impExt.prebid.Floors = nil + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := test.impExt.Clone() + test.mutator(t, test.impExt) + assert.Equal(t, test.impExtCopy, clone) + }) + } +} diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index 9c76744fac3..8e9f36e8484 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -2,6 +2,9 @@ package openrtb_ext import ( "encoding/json" + + "github.com/prebid/openrtb/v19/adcom1" + "github.com/prebid/openrtb/v19/openrtb2" ) // ExtBidResponse defines the contract for bidresponse.ext @@ -44,6 +47,8 @@ type ExtResponsePrebid struct { Modules json.RawMessage `json:"modules,omitempty"` Fledge *Fledge `json:"fledge,omitempty"` Targeting map[string]string `json:"targeting,omitempty"` + // SeatNonBid holds the array of Bids which are either rejected, no bids inside bidresponse.ext.prebid.seatnonbid + SeatNonBid []SeatNonBid `json:"seatnonbid,omitempty"` } // FledgeResponse defines the contract for bidresponse.ext.fledge @@ -96,3 +101,44 @@ const ( UserSyncIframe UserSyncType = "iframe" UserSyncPixel UserSyncType = "pixel" ) + +// NonBidObject is subset of Bid object with exact json signature +// defined at https://github.com/prebid/openrtb/blob/v19.0.0/openrtb2/bid.go +// It also contains the custom fields +type NonBidObject struct { + Price float64 `json:"price,omitempty"` + ADomain []string `json:"adomain,omitempty"` + CatTax adcom1.CategoryTaxonomy `json:"cattax,omitempty"` + Cat []string `json:"cat,omitempty"` + DealID string `json:"dealid,omitempty"` + W int64 `json:"w,omitempty"` + H int64 `json:"h,omitempty"` + Dur int64 `json:"dur,omitempty"` + MType openrtb2.MarkupType `json:"mtype,omitempty"` + + OriginalBidCPM float64 `json:"origbidcpm,omitempty"` + OriginalBidCur string `json:"origbidcur,omitempty"` +} + +// ExtResponseNonBidPrebid represents bidresponse.ext.prebid.seatnonbid[].nonbid[].ext +type ExtResponseNonBidPrebid struct { + Bid NonBidObject `json:"bid"` +} + +type NonBidExt struct { + Prebid ExtResponseNonBidPrebid `json:"prebid"` +} + +// NonBid represnts the Non Bid Reason (statusCode) for given impression ID +type NonBid struct { + ImpId string `json:"impid"` + StatusCode int `json:"statuscode"` + Ext NonBidExt `json:"ext"` +} + +// SeatNonBid is collection of NonBid objects with seat information +type SeatNonBid struct { + NonBid []NonBid `json:"nonbid"` + Seat string `json:"seat"` + Ext json.RawMessage `json:"ext"` +} diff --git a/openrtb_ext/supplyChain.go b/openrtb_ext/supplyChain.go new file mode 100644 index 00000000000..6f023542dfb --- /dev/null +++ b/openrtb_ext/supplyChain.go @@ -0,0 +1,21 @@ +package openrtb_ext + +import ( + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/ptrutil" +) + +func cloneSupplyChain(schain *openrtb2.SupplyChain) *openrtb2.SupplyChain { + if schain == nil { + return nil + } + clone := *schain + clone.Nodes = make([]openrtb2.SupplyChainNode, len(schain.Nodes)) + for i, node := range schain.Nodes { + clone.Nodes[i] = node + clone.Nodes[i].HP = ptrutil.Clone(schain.Nodes[i].HP) + } + + return &clone + +} diff --git a/openrtb_ext/supplyChain_test.go b/openrtb_ext/supplyChain_test.go new file mode 100644 index 00000000000..12fd5c337fb --- /dev/null +++ b/openrtb_ext/supplyChain_test.go @@ -0,0 +1,83 @@ +package openrtb_ext + +import ( + "encoding/json" + "testing" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +func TestCloneSupplyChain(t *testing.T) { + testCases := []struct { + name string + schain *openrtb2.SupplyChain + schainCopy *openrtb2.SupplyChain // manual copy of above prebid object to verify against + mutator func(t *testing.T, schain *openrtb2.SupplyChain) // function to modify the prebid object + }{ + { + name: "Nil", // Verify the nil case + schain: nil, + schainCopy: nil, + mutator: func(t *testing.T, schain *openrtb2.SupplyChain) {}, + }, + { + name: "General", + schain: &openrtb2.SupplyChain{ + Complete: 2, + Nodes: []openrtb2.SupplyChainNode{ + { + SID: "alpha", + Name: "Johnny", + HP: ptrutil.ToPtr[int8](5), + Ext: json.RawMessage(`{}`), + }, + { + ASI: "Oh my", + Name: "Johnny", + HP: ptrutil.ToPtr[int8](5), + Ext: json.RawMessage(`{"samson"}`), + }, + }, + Ver: "v2.5", + Ext: json.RawMessage(`{"foo": "bar"}`), + }, + schainCopy: &openrtb2.SupplyChain{ + Complete: 2, + Nodes: []openrtb2.SupplyChainNode{ + { + SID: "alpha", + Name: "Johnny", + HP: ptrutil.ToPtr[int8](5), + Ext: json.RawMessage(`{}`), + }, + { + ASI: "Oh my", + Name: "Johnny", + HP: ptrutil.ToPtr[int8](5), + Ext: json.RawMessage(`{"samson"}`), + }, + }, + Ver: "v2.5", + Ext: json.RawMessage(`{"foo": "bar"}`), + }, + mutator: func(t *testing.T, schain *openrtb2.SupplyChain) { + schain.Nodes[0].SID = "beta" + schain.Nodes[1].HP = nil + schain.Nodes[0].Ext = nil + schain.Nodes = append(schain.Nodes, openrtb2.SupplyChainNode{SID: "Gamma"}) + schain.Complete = 0 + schain.Ext = json.RawMessage(`{}`) + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + clone := cloneSupplyChain(test.schain) + test.mutator(t, test.schain) + assert.Equal(t, test.schainCopy, clone) + }) + } +} diff --git a/ortb/clone.go b/ortb/clone.go new file mode 100644 index 00000000000..c0e5a4ddada --- /dev/null +++ b/ortb/clone.go @@ -0,0 +1,267 @@ +package ortb + +import ( + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/prebid/prebid-server/util/sliceutil" +) + +func CloneApp(s *openrtb2.App) *openrtb2.App { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Cat = sliceutil.Clone(s.Cat) + c.SectionCat = sliceutil.Clone(s.SectionCat) + c.PageCat = sliceutil.Clone(s.PageCat) + c.Publisher = ClonePublisher(s.Publisher) + c.Content = CloneContent(s.Content) + c.KwArray = sliceutil.Clone(s.KwArray) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func ClonePublisher(s *openrtb2.Publisher) *openrtb2.Publisher { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Cat = sliceutil.Clone(s.Cat) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneContent(s *openrtb2.Content) *openrtb2.Content { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Producer = CloneProducer(s.Producer) + c.Cat = sliceutil.Clone(s.Cat) + c.ProdQ = ptrutil.Clone(s.ProdQ) + c.VideoQuality = ptrutil.Clone(s.VideoQuality) + c.KwArray = sliceutil.Clone(s.KwArray) + c.Data = CloneDataSlice(s.Data) + c.Network = CloneNetwork(s.Network) + c.Channel = CloneChannel(s.Channel) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneProducer(s *openrtb2.Producer) *openrtb2.Producer { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Cat = sliceutil.Clone(s.Cat) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneDataSlice(s []openrtb2.Data) []openrtb2.Data { + if s == nil { + return nil + } + + c := make([]openrtb2.Data, len(s)) + for i, d := range s { + c[i] = CloneData(d) + } + + return c +} + +func CloneData(s openrtb2.Data) openrtb2.Data { + // Shallow Copy (Value Fields) Occurred By Passing Argument By Value + + // Deep Copy (Pointers) + s.Segment = CloneSegmentSlice(s.Segment) + s.Ext = sliceutil.Clone(s.Ext) + + return s +} + +func CloneSegmentSlice(s []openrtb2.Segment) []openrtb2.Segment { + if s == nil { + return nil + } + + c := make([]openrtb2.Segment, len(s)) + for i, d := range s { + c[i] = CloneSegment(d) + } + + return c +} + +func CloneSegment(s openrtb2.Segment) openrtb2.Segment { + // Shallow Copy (Value Fields) Occurred By Passing Argument By Value + + // Deep Copy (Pointers) + s.Ext = sliceutil.Clone(s.Ext) + + return s +} + +func CloneNetwork(s *openrtb2.Network) *openrtb2.Network { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneChannel(s *openrtb2.Channel) *openrtb2.Channel { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneSite(s *openrtb2.Site) *openrtb2.Site { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Cat = sliceutil.Clone(s.Cat) + c.SectionCat = sliceutil.Clone(s.SectionCat) + c.PageCat = sliceutil.Clone(s.PageCat) + c.Publisher = ClonePublisher(s.Publisher) + c.Content = CloneContent(s.Content) + c.KwArray = sliceutil.Clone(s.KwArray) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneUser(s *openrtb2.User) *openrtb2.User { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.KwArray = sliceutil.Clone(s.KwArray) + c.Geo = CloneGeo(s.Geo) + c.Data = CloneDataSlice(s.Data) + c.EIDs = CloneEIDSlice(s.EIDs) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneGeo(s *openrtb2.Geo) *openrtb2.Geo { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} + +func CloneEIDSlice(s []openrtb2.EID) []openrtb2.EID { + if s == nil { + return nil + } + + c := make([]openrtb2.EID, len(s)) + for i, d := range s { + c[i] = CloneEID(d) + } + + return c +} + +func CloneEID(s openrtb2.EID) openrtb2.EID { + // Shallow Copy (Value Fields) Occurred By Passing Argument By Value + + // Deep Copy (Pointers) + s.UIDs = CloneUIDSlice(s.UIDs) + s.Ext = sliceutil.Clone(s.Ext) + + return s +} + +func CloneUIDSlice(s []openrtb2.UID) []openrtb2.UID { + if s == nil { + return nil + } + + c := make([]openrtb2.UID, len(s)) + for i, d := range s { + c[i] = CloneUID(d) + } + + return c +} + +func CloneUID(s openrtb2.UID) openrtb2.UID { + // Shallow Copy (Value Fields) Occurred By Passing Argument By Value + + // Deep Copy (Pointers) + s.Ext = sliceutil.Clone(s.Ext) + + return s +} + +func CloneDOOH(s *openrtb2.DOOH) *openrtb2.DOOH { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.VenueType = sliceutil.Clone(s.VenueType) + c.VenueTypeTax = ptrutil.Clone(s.VenueTypeTax) + c.Publisher = ClonePublisher(s.Publisher) + c.Content = CloneContent(s.Content) + c.Ext = sliceutil.Clone(s.Ext) + + return &c +} diff --git a/ortb/clone_test.go b/ortb/clone_test.go new file mode 100644 index 00000000000..24e43bda1e5 --- /dev/null +++ b/ortb/clone_test.go @@ -0,0 +1,763 @@ +package ortb + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/prebid/openrtb/v19/adcom1" + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +func TestCloneApp(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneApp(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.App{} + result := CloneApp(given) + assert.Equal(t, given, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.App{ + ID: "anyID", + Name: "anyName", + Bundle: "anyBundle", + Domain: "anyDomain", + StoreURL: "anyStoreURL", + CatTax: adcom1.CatTaxIABContent10, + Cat: []string{"cat1"}, + SectionCat: []string{"sectionCat1"}, + PageCat: []string{"pageCat1"}, + Ver: "anyVer", + PrivacyPolicy: 1, + Paid: 2, + Publisher: &openrtb2.Publisher{ID: "anyPublisher", Ext: json.RawMessage(`{"publisher":1}`)}, + Content: &openrtb2.Content{ID: "anyContent", Ext: json.RawMessage(`{"content":1}`)}, + Keywords: "anyKeywords", + KwArray: []string{"key1"}, + InventoryPartnerDomain: "anyInventoryPartnerDomain", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneApp(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Cat, result.Cat, "cat") + assert.NotSame(t, given.SectionCat, result.SectionCat, "sectioncat") + assert.NotSame(t, given.PageCat, result.PageCat, "pagecat") + assert.NotSame(t, given.Publisher, result.Publisher, "publisher") + assert.NotSame(t, given.Publisher.Ext, result.Publisher.Ext, "publisher-ext") + assert.NotSame(t, given.Content, result.Content, "content") + assert.NotSame(t, given.Content.Ext, result.Content.Ext, "content-ext") + assert.NotSame(t, given.KwArray, result.KwArray, "kwarray") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.App{})), + []string{ + "Cat", + "SectionCat", + "PageCat", + "Publisher", + "Content", + "KwArray", + "Ext", + }) + }) +} + +func TestClonePublisher(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := ClonePublisher(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Publisher{} + result := ClonePublisher(given) + assert.Equal(t, given, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Publisher{ + ID: "anyID", + Name: "anyName", + CatTax: adcom1.CatTaxIABContent20, + Cat: []string{"cat1"}, + Domain: "anyDomain", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := ClonePublisher(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Cat, result.Cat, "cat") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Publisher{})), + []string{ + "Cat", + "Ext", + }) + }) +} + +func TestCloneContent(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneContent(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Content{} + result := CloneContent(given) + assert.Equal(t, given, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Content{ + ID: "anyID", + Episode: 1, + Title: "anyTitle", + Series: "anySeries", + Season: "anySeason", + Artist: "anyArtist", + Genre: "anyGenre", + Album: "anyAlbum", + ISRC: "anyIsrc", + Producer: &openrtb2.Producer{ID: "anyID", Cat: []string{"anyCat"}}, + URL: "anyUrl", + CatTax: adcom1.CatTaxIABContent10, + Cat: []string{"cat1"}, + ProdQ: ptrutil.ToPtr(adcom1.ProductionProsumer), + VideoQuality: ptrutil.ToPtr(adcom1.ProductionProfessional), + Context: adcom1.ContentApp, + ContentRating: "anyContentRating", + UserRating: "anyUserRating", + QAGMediaRating: adcom1.MediaRatingAll, + Keywords: "anyKeywords", + KwArray: []string{"key1"}, + LiveStream: 2, + SourceRelationship: 3, + Len: 4, + Language: "anyLanguage", + LangB: "anyLangB", + Embeddable: 5, + Data: []openrtb2.Data{{ID: "1", Ext: json.RawMessage(`{"data":1}`)}}, + Network: &openrtb2.Network{ID: "anyNetwork", Ext: json.RawMessage(`{"network":1}`)}, + Channel: &openrtb2.Channel{ID: "anyChannel", Ext: json.RawMessage(`{"channel":1}`)}, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneContent(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Producer, result.Producer, "producer") + assert.NotSame(t, given.Producer.Cat, result.Producer.Cat, "producer-cat") + assert.NotSame(t, given.Cat, result.Cat, "cat") + assert.NotSame(t, given.ProdQ, result.ProdQ, "prodq") + assert.NotSame(t, given.VideoQuality, result.VideoQuality, "videoquality") + assert.NotSame(t, given.KwArray, result.KwArray, "kwarray") + assert.NotSame(t, given.Data, result.Data, "data") + assert.NotSame(t, given.Data[0], result.Data[0], "data-item") + assert.NotSame(t, given.Data[0].Ext, result.Data[0].Ext, "data-item-ext") + assert.NotSame(t, given.Network, result.Network, "network") + assert.NotSame(t, given.Network.Ext, result.Network.Ext, "network-ext") + assert.NotSame(t, given.Channel, result.Channel, "channel") + assert.NotSame(t, given.Channel.Ext, result.Channel.Ext, "channel-ext") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Content{})), + []string{ + "Producer", + "Cat", + "ProdQ", + "VideoQuality", + "KwArray", + "Data", + "Network", + "Channel", + "Ext", + }) + }) +} + +func TestCloneProducer(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneProducer(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Producer{} + result := CloneProducer(given) + assert.Equal(t, given, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Producer{ + ID: "anyID", + Name: "anyName", + CatTax: adcom1.CatTaxIABContent20, + Cat: []string{"cat1"}, + Domain: "anyDomain", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneProducer(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Cat, result.Cat, "cat") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Producer{})), + []string{ + "Cat", + "Ext", + }) + }) +} + +func TestCloneDataSlice(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneDataSlice(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := []openrtb2.Data{} + result := CloneDataSlice(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("one", func(t *testing.T) { + given := []openrtb2.Data{ + {ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + } + result := CloneDataSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext") + }) + + t.Run("many", func(t *testing.T) { + given := []openrtb2.Data{ + {ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + {ID: "2", Ext: json.RawMessage(`{"anyField":2}`)}, + } + result := CloneDataSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item0-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext") + assert.NotSame(t, given[1], result[1], "item1-pointer") + assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext") + }) +} + +func TestCloneData(t *testing.T) { + t.Run("populated", func(t *testing.T) { + given := openrtb2.Data{ + ID: "anyID", + Name: "anyName", + Segment: []openrtb2.Segment{{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}}, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneData(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Segment, result.Segment, "segment") + assert.NotSame(t, given.Segment[0], result.Segment[0], "segment-item") + assert.NotSame(t, given.Segment[0].Ext, result.Segment[0].Ext, "segment-item-ext") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Data{})), + []string{ + "Segment", + "Ext", + }) + }) +} + +func TestCloneSegmentSlice(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneSegmentSlice(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := []openrtb2.Segment{} + result := CloneSegmentSlice(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("one", func(t *testing.T) { + given := []openrtb2.Segment{ + {ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + } + result := CloneSegmentSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext") + }) + + t.Run("many", func(t *testing.T) { + given := []openrtb2.Segment{ + {Ext: json.RawMessage(`{"anyField":1}`)}, + {Ext: json.RawMessage(`{"anyField":2}`)}, + } + result := CloneSegmentSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item0-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext") + assert.NotSame(t, given[1], result[1], "item1-pointer") + assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext") + }) +} + +func TestCloneSegment(t *testing.T) { + t.Run("populated", func(t *testing.T) { + given := openrtb2.Segment{ + ID: "anyID", + Name: "anyName", + Value: "anyValue", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneSegment(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Segment{})), + []string{ + "Ext", + }) + }) +} + +func TestCloneNetwork(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneNetwork(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Network{} + result := CloneNetwork(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Network{ + ID: "anyID", + Name: "anyName", + Domain: "anyDomain", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneNetwork(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Network{})), + []string{ + "Ext", + }) + }) +} + +func TestCloneChannel(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneChannel(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Channel{} + result := CloneChannel(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Channel{ + ID: "anyID", + Name: "anyName", + Domain: "anyDomain", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneChannel(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Channel{})), + []string{ + "Ext", + }) + }) +} + +func TestCloneSite(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneSite(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Site{} + result := CloneSite(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Site{ + ID: "anyID", + Name: "anyName", + Domain: "anyDomain", + CatTax: adcom1.CatTaxIABContent10, + Cat: []string{"cat1"}, + SectionCat: []string{"sectionCat1"}, + PageCat: []string{"pageCat1"}, + Page: "anyPage", + Ref: "anyRef", + Search: "anySearch", + Mobile: 1, + PrivacyPolicy: 2, + Publisher: &openrtb2.Publisher{ID: "anyPublisher", Ext: json.RawMessage(`{"publisher":1}`)}, + Content: &openrtb2.Content{ID: "anyContent", Ext: json.RawMessage(`{"content":1}`)}, + Keywords: "anyKeywords", + KwArray: []string{"key1"}, + InventoryPartnerDomain: "anyInventoryPartnerDomain", + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneSite(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Cat, result.Cat, "cat") + assert.NotSame(t, given.SectionCat, result.SectionCat, "sectioncat") + assert.NotSame(t, given.PageCat, result.PageCat, "pagecat") + assert.NotSame(t, given.Publisher, result.Publisher, "publisher") + assert.NotSame(t, given.Publisher.Ext, result.Publisher.Ext, "publisher-ext") + assert.NotSame(t, given.Content, result.Content, "content") + assert.NotSame(t, given.Content.Ext, result.Content.Ext, "content-ext") + assert.NotSame(t, given.KwArray, result.KwArray, "kwarray") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Site{})), + []string{ + "Cat", + "SectionCat", + "PageCat", + "Publisher", + "Content", + "KwArray", + "Ext", + }) + }) +} + +func TestCloneUser(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneUser(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.User{} + result := CloneUser(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.User{ + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 1, + Gender: "anyGender", + Keywords: "anyKeywords", + KwArray: []string{"key1"}, + CustomData: "anyCustomData", + Geo: &openrtb2.Geo{Lat: 1.2, Lon: 2.3, Ext: json.RawMessage(`{"geo":1}`)}, + Data: []openrtb2.Data{{ID: "1", Ext: json.RawMessage(`{"data":1}`)}}, + Consent: "anyConsent", + EIDs: []openrtb2.EID{{Source: "1", Ext: json.RawMessage(`{"eid":1}`)}}, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneUser(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.KwArray, result.KwArray, "cat") + assert.NotSame(t, given.Geo, result.Geo, "geo") + assert.NotSame(t, given.Geo.Ext, result.Geo.Ext, "geo-ext") + assert.NotSame(t, given.Data[0], result.Data[0], "data-item") + assert.NotSame(t, given.Data[0].Ext, result.Data[0].Ext, "data-item-ext") + assert.NotSame(t, given.EIDs[0], result.EIDs[0], "eids-item") + assert.NotSame(t, given.EIDs[0].Ext, result.EIDs[0].Ext, "eids-item-ext") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.User{})), + []string{ + "KwArray", + "Geo", + "Data", + "EIDs", + "Ext", + }) + }) +} + +func TestCloneGeo(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneGeo(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Geo{} + result := CloneGeo(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Geo{ + Lat: 1.234, + Lon: 5.678, + Type: adcom1.LocationGPS, + Accuracy: 1, + LastFix: 2, + IPService: adcom1.LocationServiceIP2Location, + Country: "anyCountry", + Region: "anyRegion", + RegionFIPS104: "anyRegionFIPS104", + Metro: "anyMetro", + City: "anyCity", + ZIP: "anyZIP", + UTCOffset: 3, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneGeo(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Geo{})), + []string{ + "Ext", + }) + }) +} + +func TestCloneEIDSlice(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneEIDSlice(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := []openrtb2.EID{} + result := CloneEIDSlice(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("one", func(t *testing.T) { + given := []openrtb2.EID{ + {Source: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + } + result := CloneEIDSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext") + }) + + t.Run("many", func(t *testing.T) { + given := []openrtb2.EID{ + {Source: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + {Source: "2", Ext: json.RawMessage(`{"anyField":2}`)}, + } + result := CloneEIDSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item0-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext") + assert.NotSame(t, given[1], result[1], "item1-pointer") + assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext") + }) +} + +func TestCloneEID(t *testing.T) { + t.Run("populated", func(t *testing.T) { + given := openrtb2.EID{ + Source: "anySource", + UIDs: []openrtb2.UID{{ID: "1", Ext: json.RawMessage(`{"uid":1}`)}}, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneEID(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.UIDs, result.UIDs, "uids") + assert.NotSame(t, given.UIDs[0], result.UIDs[0], "uids-item") + assert.NotSame(t, given.UIDs[0].Ext, result.UIDs[0].Ext, "uids-item-ext") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.EID{})), + []string{ + "UIDs", + "Ext", + }) + }) +} + +func TestCloneUIDSlice(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneUIDSlice(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := []openrtb2.UID{} + result := CloneUIDSlice(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("one", func(t *testing.T) { + given := []openrtb2.UID{ + {ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + } + result := CloneUIDSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext") + }) + + t.Run("many", func(t *testing.T) { + given := []openrtb2.UID{ + {ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}, + {ID: "2", Ext: json.RawMessage(`{"anyField":2}`)}, + } + result := CloneUIDSlice(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given[0], result[0], "item0-pointer") + assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext") + assert.NotSame(t, given[1], result[1], "item1-pointer") + assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext") + }) +} + +func TestCloneUID(t *testing.T) { + t.Run("populated", func(t *testing.T) { + given := openrtb2.UID{ + ID: "anyID", + AType: adcom1.AgentTypePerson, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneUID(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.UID{})), + []string{ + "Ext", + }) + }) +} + +func TestCloneDOOH(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneDOOH(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.DOOH{} + result := CloneDOOH(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.DOOH{ + ID: "anyID", + Name: "anyName", + VenueType: []string{"venue1"}, + VenueTypeTax: ptrutil.ToPtr(adcom1.VenueTaxonomyAdCom), + Publisher: &openrtb2.Publisher{ID: "anyPublisher", Ext: json.RawMessage(`{"publisher":1}`)}, + Domain: "anyDomain", + Keywords: "anyKeywords", + Content: &openrtb2.Content{ID: "anyContent", Ext: json.RawMessage(`{"content":1}`)}, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneDOOH(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.VenueType, result.VenueType, "venuetype") + assert.NotSame(t, given.VenueTypeTax, result.VenueTypeTax, "venuetypetax") + assert.NotSame(t, given.Publisher, result.Publisher, "publisher") + assert.NotSame(t, given.Publisher.Ext, result.Publisher.Ext, "publisher-ext") + assert.NotSame(t, given.Content, result.Content, "content") + assert.NotSame(t, given.Content.Ext, result.Content.Ext, "content-ext") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.DOOH{})), + []string{ + "VenueType", + "VenueTypeTax", + "Publisher", + "Content", + "Ext", + }) + }) +} + +// discoverPointerFields returns the names of all fields of an object that are +// pointers and would need to be cloned. This method is specific to types which can +// appear within an OpenRTB data model object. +func discoverPointerFields(t reflect.Type) []string { + var fields []string + for _, f := range reflect.VisibleFields(t) { + if f.Type.Kind() == reflect.Slice || f.Type.Kind() == reflect.Pointer { + fields = append(fields, f.Name) + } + } + return fields +} diff --git a/ortb/default.go b/ortb/default.go index 1581961b02f..cd9d8c24759 100644 --- a/ortb/default.go +++ b/ortb/default.go @@ -46,11 +46,32 @@ func setDefaultsTargeting(targeting *openrtb_ext.ExtRequestTargeting) bool { modified := false - if targeting.PriceGranularity == nil || len(targeting.PriceGranularity.Ranges) == 0 { - targeting.PriceGranularity = ptrutil.ToPtr(openrtb_ext.NewPriceGranularityDefault()) - modified = true - } else if setDefaultsPriceGranularity(targeting.PriceGranularity) { + if newPG, updated := setDefaultsPriceGranularity(targeting.PriceGranularity); updated { modified = true + targeting.PriceGranularity = newPG + } + + // If price granularity is not specified in request then default one should be set. + // Default price granularity can be overwritten for video, banner or native bid type + // only in case targeting.MediaTypePriceGranularity.Video|Banner|Native != nil. + + if targeting.MediaTypePriceGranularity.Video != nil { + if newVideoPG, updated := setDefaultsPriceGranularity(targeting.MediaTypePriceGranularity.Video); updated { + modified = true + targeting.MediaTypePriceGranularity.Video = newVideoPG + } + } + if targeting.MediaTypePriceGranularity.Banner != nil { + if newBannerPG, updated := setDefaultsPriceGranularity(targeting.MediaTypePriceGranularity.Banner); updated { + modified = true + targeting.MediaTypePriceGranularity.Banner = newBannerPG + } + } + if targeting.MediaTypePriceGranularity.Native != nil { + if newNativePG, updated := setDefaultsPriceGranularity(targeting.MediaTypePriceGranularity.Native); updated { + modified = true + targeting.MediaTypePriceGranularity.Native = newNativePG + } } if targeting.IncludeWinners == nil { @@ -66,7 +87,12 @@ func setDefaultsTargeting(targeting *openrtb_ext.ExtRequestTargeting) bool { return modified } -func setDefaultsPriceGranularity(pg *openrtb_ext.PriceGranularity) bool { +func setDefaultsPriceGranularity(pg *openrtb_ext.PriceGranularity) (*openrtb_ext.PriceGranularity, bool) { + if pg == nil || len(pg.Ranges) == 0 { + pg = ptrutil.ToPtr(openrtb_ext.NewPriceGranularityDefault()) + return pg, true + } + modified := false if pg.Precision == nil { @@ -78,7 +104,7 @@ func setDefaultsPriceGranularity(pg *openrtb_ext.PriceGranularity) bool { modified = true } - return modified + return pg, modified } func setDefaultsPriceGranularityRange(ranges []openrtb_ext.GranularityRange) bool { diff --git a/ortb/default_test.go b/ortb/default_test.go index 0ea35f6e119..04eeeebdcb6 100644 --- a/ortb/default_test.go +++ b/ortb/default_test.go @@ -36,7 +36,7 @@ func TestSetDefaults(t *testing.T) { { name: "targeting", // tests integration with setDefaultsTargeting givenRequest: openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{}}}`)}, - expectedRequest: openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true}}}`)}, + expectedRequest: openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`)}, }, { name: "imp", // tests integration with setDefaultsImp @@ -152,6 +152,40 @@ func TestSetDefaultsTargeting(t *testing.T) { }, expectedModified: true, }, + { + name: "populated-ranges-nil-mediatypepricegranularity-video-banner-native", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: nil, + }, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: nil, + }, + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: nil, + }, + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: nil, + }, + }, + }, + expectedTargeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: &defaultGranularity, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &defaultGranularity, + Banner: &defaultGranularity, + Native: &defaultGranularity, + }, + IncludeWinners: ptrutil.ToPtr(DefaultTargetingIncludeWinners), + IncludeBidderKeys: ptrutil.ToPtr(DefaultTargetingIncludeBidderKeys), + }, + expectedModified: true, + }, { name: "populated-ranges-empty", givenTargeting: &openrtb_ext.ExtRequestTargeting{ @@ -167,6 +201,40 @@ func TestSetDefaultsTargeting(t *testing.T) { }, expectedModified: true, }, + { + name: "populated-ranges-empty-mediatypepricegranularity-video-banner-native", + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{}, + }, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{}, + }, + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{}, + }, + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{}, + }, + }, + }, + expectedTargeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: &defaultGranularity, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &defaultGranularity, + Banner: &defaultGranularity, + Native: &defaultGranularity, + }, + IncludeWinners: ptrutil.ToPtr(DefaultTargetingIncludeWinners), + IncludeBidderKeys: ptrutil.ToPtr(DefaultTargetingIncludeBidderKeys), + }, + expectedModified: true, + }, { name: "populated-full", // no defaults set givenTargeting: &openrtb_ext.ExtRequestTargeting{ @@ -187,6 +255,51 @@ func TestSetDefaultsTargeting(t *testing.T) { }, expectedModified: false, }, + { + name: "populated-full-mediatypepricegranularity-video-banner", // no defaults set + givenTargeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}, + }, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}, + }, + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}, + }, + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}, + }, + }, + IncludeWinners: ptrutil.ToPtr(false), + IncludeBidderKeys: ptrutil.ToPtr(false), + }, + expectedTargeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}, + }, + MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ + Video: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}}, + Banner: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}}, + Native: &openrtb_ext.PriceGranularity{ + Precision: ptrutil.ToPtr(4), + Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}}}, + }, + IncludeWinners: ptrutil.ToPtr(false), + IncludeBidderKeys: ptrutil.ToPtr(false), + }, + expectedModified: false, + }, { name: "setDefaultsPriceGranularity-integration", givenTargeting: &openrtb_ext.ExtRequestTargeting{ @@ -277,9 +390,9 @@ func TestSetDefaultsPriceGranularity(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - actualModified := setDefaultsPriceGranularity(test.givenGranularity) + pg, actualModified := setDefaultsPriceGranularity(test.givenGranularity) assert.Equal(t, test.expectedModified, actualModified) - assert.Equal(t, test.expectedGranularity, test.givenGranularity) + assert.Equal(t, test.expectedGranularity, pg) }) } } diff --git a/pbs/usersync.go b/pbs/usersync.go index 748581af759..7b468cb039d 100644 --- a/pbs/usersync.go +++ b/pbs/usersync.go @@ -58,6 +58,8 @@ func (deps *UserSyncDeps) VerifyRecaptcha(response string) error { func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { optout := r.FormValue("optout") rr := r.FormValue("g-recaptcha-response") + encoder := usersync.Base64Encoder{} + decoder := usersync.Base64Decoder{} if rr == "" { http.Redirect(w, r, fmt.Sprintf("%s/static/optout.html", deps.ExternalUrl), http.StatusMovedPermanently) @@ -73,10 +75,18 @@ func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httpr return } - pc := usersync.ParseCookieFromRequest(r, deps.HostCookieConfig) + // Read Cookie + pc := usersync.ReadCookie(r, decoder, deps.HostCookieConfig) + usersync.SyncHostCookie(r, pc, deps.HostCookieConfig) pc.SetOptOut(optout != "") - pc.SetCookieOnResponse(w, false, deps.HostCookieConfig, deps.HostCookieConfig.TTLDuration()) + // Write Cookie + encodedCookie, err := pc.PrepareCookieForWrite(deps.HostCookieConfig, encoder) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + usersync.WriteCookie(w, encodedCookie, deps.HostCookieConfig, false) if optout == "" { http.Redirect(w, r, deps.HostCookieConfig.OptInURL, http.StatusMovedPermanently) diff --git a/privacy/activity.go b/privacy/activity.go new file mode 100644 index 00000000000..6ca48ae0b93 --- /dev/null +++ b/privacy/activity.go @@ -0,0 +1,38 @@ +package privacy + +// Activity defines privileges which can be controlled directly by the publisher or via privacy policies. +type Activity int + +const ( + ActivitySyncUser Activity = iota + 1 + ActivityFetchBids + ActivityEnrichUserFPD + ActivityReportAnalytics + ActivityTransmitUserFPD + ActivityTransmitPreciseGeo + ActivityTransmitUniqueRequestIds + ActivityTransmitTids +) + +func (a Activity) String() string { + switch a { + case ActivitySyncUser: + return "syncUser" + case ActivityFetchBids: + return "fetchBids" + case ActivityEnrichUserFPD: + return "enrichUfpd" + case ActivityReportAnalytics: + return "reportAnalytics" + case ActivityTransmitUserFPD: + return "transmitUfpd" + case ActivityTransmitPreciseGeo: + return "transmitPreciseGeo" + case ActivityTransmitUniqueRequestIds: + return "transmitUniqueRequestIds" + case ActivityTransmitTids: + return "transmitTid" + } + + return "" +} diff --git a/privacy/activitycontrol.go b/privacy/activitycontrol.go new file mode 100644 index 00000000000..5d68ce71770 --- /dev/null +++ b/privacy/activitycontrol.go @@ -0,0 +1,148 @@ +package privacy + +import ( + "github.com/prebid/prebid-server/config" +) + +type ActivityResult int + +const ( + ActivityAbstain ActivityResult = iota + ActivityAllow + ActivityDeny +) + +const defaultActivityResult = true + +type ActivityControl struct { + plans map[Activity]ActivityPlan +} + +func NewActivityControl(privacyConf *config.AccountPrivacy) (ActivityControl, error) { + ac := ActivityControl{} + var err error + + if privacyConf == nil || privacyConf.AllowActivities == nil { + return ac, nil + } + + plans := make(map[Activity]ActivityPlan) + + plans[ActivitySyncUser], err = buildEnforcementPlan(privacyConf.AllowActivities.SyncUser) + if err != nil { + return ac, err + } + plans[ActivityFetchBids], err = buildEnforcementPlan(privacyConf.AllowActivities.FetchBids) + if err != nil { + return ac, err + } + plans[ActivityEnrichUserFPD], err = buildEnforcementPlan(privacyConf.AllowActivities.EnrichUserFPD) + if err != nil { + return ac, err + } + plans[ActivityReportAnalytics], err = buildEnforcementPlan(privacyConf.AllowActivities.ReportAnalytics) + if err != nil { + return ac, err + } + plans[ActivityTransmitUserFPD], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitUserFPD) + if err != nil { + return ac, err + } + plans[ActivityTransmitPreciseGeo], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitPreciseGeo) + if err != nil { + return ac, err + } + plans[ActivityTransmitUniqueRequestIds], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitUniqueRequestIds) + if err != nil { + return ac, err + } + plans[ActivityTransmitTids], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitTids) + if err != nil { + return ac, err + } + + ac.plans = plans + + return ac, nil +} + +func buildEnforcementPlan(activity config.Activity) (ActivityPlan, error) { + ef := ActivityPlan{} + rules, err := activityRulesToEnforcementRules(activity.Rules) + if err != nil { + return ef, err + } + ef.defaultResult = activityDefaultToDefaultResult(activity.Default) + ef.rules = rules + return ef, nil +} + +func activityRulesToEnforcementRules(rules []config.ActivityRule) ([]Rule, error) { + var enfRules []Rule + + for _, r := range rules { + var result ActivityResult + if r.Allow { + result = ActivityAllow + } else { + result = ActivityDeny + } + + componentName, err := conditionToRuleComponentNames(r.Condition.ComponentName) + if err != nil { + return nil, err + } + + er := ComponentEnforcementRule{ + result: result, + componentName: componentName, + componentType: r.Condition.ComponentType, + } + enfRules = append(enfRules, er) + } + return enfRules, nil +} + +func conditionToRuleComponentNames(conditions []string) ([]Component, error) { + sn := make([]Component, 0) + for _, condition := range conditions { + scope, err := ParseComponent(condition) + if err != nil { + return nil, err + } + sn = append(sn, scope) + } + return sn, nil +} + +func activityDefaultToDefaultResult(activityDefault *bool) bool { + if activityDefault == nil { + return defaultActivityResult + } + return *activityDefault +} + +func (e ActivityControl) Allow(activity Activity, target Component) bool { + plan, planDefined := e.plans[activity] + + if !planDefined { + return defaultActivityResult + } + + return plan.Evaluate(target) +} + +type ActivityPlan struct { + defaultResult bool + rules []Rule +} + +func (p ActivityPlan) Evaluate(target Component) bool { + for _, rule := range p.rules { + result := rule.Evaluate(target) + if result == ActivityDeny || result == ActivityAllow { + return result == ActivityAllow + } + } + return p.defaultResult +} diff --git a/privacy/activitycontrol_test.go b/privacy/activitycontrol_test.go new file mode 100644 index 00000000000..db5d588de44 --- /dev/null +++ b/privacy/activitycontrol_test.go @@ -0,0 +1,272 @@ +package privacy + +import ( + "errors" + "testing" + + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +func TestNewActivityControl(t *testing.T) { + testCases := []struct { + name string + privacyConf config.AccountPrivacy + activityControl ActivityControl + err error + }{ + { + name: "privacy_config_is_empty", + privacyConf: config.AccountPrivacy{}, + activityControl: ActivityControl{plans: nil}, + err: nil, + }, + { + name: "privacy_config_is_specified_and_correct", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + SyncUser: getDefaultActivityConfig(), + FetchBids: getDefaultActivityConfig(), + EnrichUserFPD: getDefaultActivityConfig(), + ReportAnalytics: getDefaultActivityConfig(), + TransmitUserFPD: getDefaultActivityConfig(), + TransmitPreciseGeo: getDefaultActivityConfig(), + TransmitUniqueRequestIds: getDefaultActivityConfig(), + TransmitTids: getDefaultActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: map[Activity]ActivityPlan{ + ActivitySyncUser: getDefaultActivityPlan(), + ActivityFetchBids: getDefaultActivityPlan(), + ActivityEnrichUserFPD: getDefaultActivityPlan(), + ActivityReportAnalytics: getDefaultActivityPlan(), + ActivityTransmitUserFPD: getDefaultActivityPlan(), + ActivityTransmitPreciseGeo: getDefaultActivityPlan(), + ActivityTransmitUniqueRequestIds: getDefaultActivityPlan(), + ActivityTransmitTids: getDefaultActivityPlan(), + }}, + err: nil, + }, + { + name: "privacy_config_is_specified_and_SyncUser_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + SyncUser: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_FetchBids_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + FetchBids: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_EnrichUserFPD_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + EnrichUserFPD: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_ReportAnalytics_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + ReportAnalytics: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_TransmitUserFPD_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitUserFPD: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_TransmitPreciseGeo_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitPreciseGeo: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_TransmitUniqueRequestIds_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitUniqueRequestIds: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "privacy_config_is_specified_and_TransmitTids_is_incorrect", + privacyConf: config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitTids: getIncorrectActivityConfig(), + }, + }, + activityControl: ActivityControl{plans: nil}, + err: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actualAC, actualErr := NewActivityControl(&test.privacyConf) + if test.err == nil { + assert.Equal(t, test.activityControl, actualAC) + assert.NoError(t, actualErr) + } else { + assert.EqualError(t, actualErr, test.err.Error()) + } + }) + } +} + +func TestActivityDefaultToDefaultResult(t *testing.T) { + testCases := []struct { + name string + activityDefault *bool + expectedResult bool + }{ + { + name: "nil", + activityDefault: nil, + expectedResult: true, + }, + { + name: "true", + activityDefault: ptrutil.ToPtr(true), + expectedResult: true, + }, + { + name: "false", + activityDefault: ptrutil.ToPtr(false), + expectedResult: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actualResult := activityDefaultToDefaultResult(test.activityDefault) + assert.Equal(t, test.expectedResult, actualResult) + }) + } +} + +func TestAllowActivityControl(t *testing.T) { + + testCases := []struct { + name string + activityControl ActivityControl + activity Activity + target Component + activityResult bool + }{ + { + name: "plans_is_nil", + activityControl: ActivityControl{plans: nil}, + activity: ActivityFetchBids, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: true, + }, + { + name: "activity_not_defined", + activityControl: ActivityControl{plans: map[Activity]ActivityPlan{ + ActivitySyncUser: getDefaultActivityPlan()}}, + activity: ActivityFetchBids, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: true, + }, + { + name: "activity_defined_but_not_found_default_returned", + activityControl: ActivityControl{plans: map[Activity]ActivityPlan{ + ActivityFetchBids: getDefaultActivityPlan()}}, + activity: ActivityFetchBids, + target: Component{Type: "bidder", Name: "bidderB"}, + activityResult: true, + }, + { + name: "activity_defined_and_allowed", + activityControl: ActivityControl{plans: map[Activity]ActivityPlan{ + ActivityFetchBids: getDefaultActivityPlan()}}, + activity: ActivityFetchBids, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: true, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actualResult := test.activityControl.Allow(test.activity, test.target) + assert.Equal(t, test.activityResult, actualResult) + + }) + } +} + +// constants +func getDefaultActivityConfig() config.Activity { + return config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: true, + Condition: config.ActivityCondition{ + ComponentName: []string{"bidderA"}, + ComponentType: []string{"bidder"}, + }, + }, + }, + } +} + +func getDefaultActivityPlan() ActivityPlan { + return ActivityPlan{ + defaultResult: true, + rules: []Rule{ + ComponentEnforcementRule{ + result: ActivityAllow, + componentName: []Component{ + {Type: "bidder", Name: "bidderA"}, + }, + componentType: []string{"bidder"}, + }, + }, + } +} + +func getIncorrectActivityConfig() config.Activity { + return config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: true, + Condition: config.ActivityCondition{ + ComponentName: []string{"bidder.bidderA.bidderB"}, + ComponentType: []string{"bidder"}, + }, + }, + }, + } +} diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index 6333e2ada0f..fbafd8a8a2e 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -1,6 +1,7 @@ package ccpa import ( + "errors" "fmt" gpplib "github.com/prebid/go-gpp" @@ -8,6 +9,7 @@ import ( "github.com/prebid/openrtb/v19/openrtb2" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + gppPolicy "github.com/prebid/prebid-server/privacy/gpp" ) // Policy represents the CCPA regulatory information from an OpenRTB bid request. @@ -18,33 +20,25 @@ type Policy struct { // ReadFromRequestWrapper extracts the CCPA regulatory information from an OpenRTB bid request. func ReadFromRequestWrapper(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (Policy, error) { - var consent string var noSaleBidders []string - var warn error = nil + var gppSIDs []int8 + var requestUSPrivacy string + var warn error if req == nil || req.BidRequest == nil { return Policy{}, nil } if req.BidRequest.Regs != nil { - for _, s := range req.BidRequest.Regs.GPPSID { - if s == int8(gppConstants.SectionUSPV1) { - for i, id := range gpp.SectionTypes { - if id == gppConstants.SectionUSPV1 { - consent = gpp.Sections[i].GetValue() - } - } - } - } - if req.BidRequest.Regs.USPrivacy != "" { - if consent == "" { - consent = req.BidRequest.Regs.USPrivacy - } else if consent != req.BidRequest.Regs.USPrivacy { - warn = &errortypes.Warning{ - Message: "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp", - WarningCode: errortypes.InvalidPrivacyConsentWarningCode} - } - } + requestUSPrivacy = req.BidRequest.Regs.USPrivacy + gppSIDs = req.BidRequest.Regs.GPPSID + } + + consent, err := SelectCCPAConsent(requestUSPrivacy, gpp, gppSIDs) + if err != nil { + warn = &errortypes.Warning{ + Message: "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode} } if consent == "" { @@ -100,6 +94,29 @@ func (p Policy) Write(req *openrtb_ext.RequestWrapper) error { return nil } +func SelectCCPAConsent(requestUSPrivacy string, gpp gpplib.GppContainer, gppSIDs []int8) (string, error) { + var consent string + var err error + + if len(gpp.SectionTypes) > 0 { + if gppPolicy.IsSIDInList(gppSIDs, gppConstants.SectionUSPV1) { + if i := gppPolicy.IndexOfSID(gpp, gppConstants.SectionUSPV1); i >= 0 { + consent = gpp.Sections[i].GetValue() + } + } + } + + if requestUSPrivacy != "" { + if consent == "" { + consent = requestUSPrivacy + } else if consent != requestUSPrivacy { + err = errors.New("request.us_privacy consent does not match uspv1") + } + } + + return consent, err +} + func setPrebidNoSale(noSaleBidders []string, ext *openrtb_ext.RequestExt) { if len(noSaleBidders) == 0 { setPrebidNoSaleClear(ext) diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index cd494ccfa7a..3a1433333c0 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -2,6 +2,7 @@ package ccpa import ( "encoding/json" + "errors" "testing" gpplib "github.com/prebid/go-gpp" @@ -165,7 +166,7 @@ func TestReadFromRequestWrapper(t *testing.T) { }, giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}}, expectedPolicy: Policy{ - Consent: "present", + Consent: "gppContainerConsent", NoSaleBidders: []string{"a", "b"}, }, }, @@ -179,7 +180,7 @@ func TestReadFromRequestWrapper(t *testing.T) { }, giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}}, expectedPolicy: Policy{ - Consent: "present", + Consent: "gppContainerConsent", NoSaleBidders: []string{"a", "b"}, }, }, @@ -193,7 +194,7 @@ func TestReadFromRequestWrapper(t *testing.T) { }, giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}}, expectedPolicy: Policy{ - Consent: "present", + Consent: "gppContainerConsent", NoSaleBidders: []string{"a", "b"}, }, expectedError: true, @@ -816,6 +817,87 @@ func TestBuildExtWrite(t *testing.T) { } } +func TestSelectCCPAConsent(t *testing.T) { + type testInput struct { + requestUSPrivacy string + gpp gpplib.GppContainer + gppSIDs []int8 + } + testCases := []struct { + desc string + in testInput + expectedCCPA string + expectedErr error + }{ + { + desc: "SectionUSPV1 in both GPP_SID and GPP container. Consent equal to request US_Privacy. Expect valid string and nil error", + in: testInput{ + requestUSPrivacy: "gppContainerConsent", + gpp: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}}, + gppSIDs: []int8{int8(6)}, + }, + expectedCCPA: "gppContainerConsent", + expectedErr: nil, + }, + { + desc: "No SectionUSPV1 in GPP_SID array expect request US_Privacy", + in: testInput{ + requestUSPrivacy: "requestConsent", + gpp: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}}, + gppSIDs: []int8{int8(2), int8(4)}, + }, + expectedCCPA: "requestConsent", + expectedErr: nil, + }, + { + desc: "No SectionUSPV1 in gpp.SectionTypes array expect request US_Privacy", + in: testInput{ + requestUSPrivacy: "requestConsent", + gpp: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{}, Sections: []gpplib.Section{upsv1Section}}, + gppSIDs: []int8{int8(6)}, + }, + expectedCCPA: "requestConsent", + expectedErr: nil, + }, + { + desc: "No SectionUSPV1 in GPP_SID array, blank request US_Privacy, expect blank consent", + in: testInput{ + requestUSPrivacy: "", + gpp: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}}, + gppSIDs: []int8{int8(2), int8(4)}, + }, + expectedCCPA: "", + expectedErr: nil, + }, + { + desc: "No SectionUSPV1 in gpp.SectionTypes array, blank request US_Privacy, expect blank consent", + in: testInput{ + requestUSPrivacy: "", + gpp: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{}, Sections: []gpplib.Section{upsv1Section}}, + gppSIDs: []int8{int8(6)}, + }, + expectedCCPA: "", + expectedErr: nil, + }, + { + desc: "SectionUSPV1 in both GPP_SID and GPP container. Consent equal to request US_Privacy. Expect valid string and nil error", + in: testInput{ + requestUSPrivacy: "requestConsent", + gpp: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}}, + gppSIDs: []int8{int8(6)}, + }, + expectedCCPA: "gppContainerConsent", + expectedErr: errors.New("request.us_privacy consent does not match uspv1"), + }, + } + for _, tc := range testCases { + out, outErr := SelectCCPAConsent(tc.in.requestUSPrivacy, tc.in.gpp, tc.in.gppSIDs) + + assert.Equal(t, tc.expectedCCPA, out, tc.desc) + assert.Equal(t, tc.expectedErr, outErr, tc.desc) + } +} + func assertError(t *testing.T, expectError bool, err error, description string) { t.Helper() if expectError { @@ -825,7 +907,7 @@ func assertError(t *testing.T, expectError bool, err error, description string) } } -var upsv1Section mockGPPSection = mockGPPSection{sectionID: 6, value: "present"} +var upsv1Section mockGPPSection = mockGPPSection{sectionID: 6, value: "gppContainerConsent"} var tcf1Section mockGPPSection = mockGPPSection{sectionID: 2, value: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"} type mockGPPSection struct { diff --git a/privacy/component.go b/privacy/component.go new file mode 100644 index 00000000000..a203c6bdb3b --- /dev/null +++ b/privacy/component.go @@ -0,0 +1,65 @@ +package privacy + +import ( + "errors" + "fmt" + "strings" +) + +const ( + ComponentTypeBidder = "bidder" + ComponentTypeAnalytics = "analytics" + ComponentTypeRealTimeData = "rtd" + ComponentTypeGeneral = "general" +) + +type Component struct { + Type string + Name string +} + +func (c Component) Matches(target Component) bool { + return strings.EqualFold(c.Type, target.Type) && (c.Name == "*" || strings.EqualFold(c.Name, target.Name)) +} + +var ErrComponentEmpty = errors.New("unable to parse empty component") + +func ParseComponent(v string) (Component, error) { + if len(v) == 0 { + return Component{}, ErrComponentEmpty + } + + split := strings.Split(v, ".") + + if len(split) == 2 { + if !validComponentType(split[0]) { + return Component{}, fmt.Errorf("unable to parse component (invalid type): %s", v) + } + return Component{ + Type: split[0], + Name: split[1], + }, nil + } + + if len(split) == 1 { + return Component{ + Type: ComponentTypeBidder, + Name: split[0], + }, nil + } + + return Component{}, fmt.Errorf("unable to parse component: %s", v) +} + +func validComponentType(t string) bool { + t = strings.ToLower(t) + + if t == ComponentTypeBidder || + t == ComponentTypeAnalytics || + t == ComponentTypeRealTimeData || + t == ComponentTypeGeneral { + return true + } + + return false +} diff --git a/privacy/component_test.go b/privacy/component_test.go new file mode 100644 index 00000000000..38aa105d602 --- /dev/null +++ b/privacy/component_test.go @@ -0,0 +1,124 @@ +package privacy + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComponentMatches(t *testing.T) { + testCases := []struct { + name string + given Component + target Component + result bool + }{ + { + name: "full", + given: Component{Type: "a", Name: "b"}, + target: Component{Type: "a", Name: "b"}, + result: true, + }, + { + name: "name-wildcard", + given: Component{Type: "a", Name: "*"}, + target: Component{Type: "a", Name: "b"}, + result: true, + }, + { + name: "different", + given: Component{Type: "a", Name: "b"}, + target: Component{Type: "foo", Name: "bar"}, + result: false, + }, + { + name: "different-type", + given: Component{Type: "a", Name: "b"}, + target: Component{Type: "foo", Name: "b"}, + result: false, + }, + { + name: "different-name", + given: Component{Type: "a", Name: "b"}, + target: Component{Type: "a", Name: "foo"}, + result: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.result, test.given.Matches(test.target)) + }) + } +} + +func TestParseComponent(t *testing.T) { + testCases := []struct { + name string + component string + expected Component + expectedError error + }{ + { + name: "empty", + component: "", + expected: Component{}, + expectedError: errors.New("unable to parse empty component"), + }, + { + name: "too-many-parts", + component: "bidder.bidderA.bidderB", + expected: Component{}, + expectedError: errors.New("unable to parse component: bidder.bidderA.bidderB"), + }, + { + name: "type-bidder", + component: "bidder.bidderA", + expected: Component{Type: "bidder", Name: "bidderA"}, + expectedError: nil, + }, + { + name: "type-analytics", + component: "analytics.bidderA", + expected: Component{Type: "analytics", Name: "bidderA"}, + expectedError: nil, + }, + { + name: "type-default", + component: "bidderA", + expected: Component{Type: "bidder", Name: "bidderA"}, + expectedError: nil, + }, + { + name: "type-rtd", + component: "rtd.test", + expected: Component{Type: "rtd", Name: "test"}, + expectedError: nil, + }, + { + name: "type-general", + component: "general.test", + expected: Component{Type: "general", Name: "test"}, + expectedError: nil, + }, + { + name: "type-invalid", + component: "invalid.test", + expected: Component{}, + expectedError: errors.New("unable to parse component (invalid type): invalid.test"), + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actualSN, actualErr := ParseComponent(test.component) + if test.expectedError == nil { + assert.Equal(t, test.expected, actualSN) + assert.NoError(t, actualErr) + } else { + assert.EqualError(t, actualErr, test.expectedError.Error()) + } + }) + } +} diff --git a/privacy/enforcement.go b/privacy/enforcement.go index e1669f78282..8074d96acf3 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -1,6 +1,9 @@ package privacy -import "github.com/prebid/openrtb/v19/openrtb2" +import ( + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/config" +) // Enforcement represents the privacy policies to enforce for an OpenRTB bid request. type Enforcement struct { @@ -9,22 +12,39 @@ type Enforcement struct { GDPRGeo bool GDPRID bool LMT bool + + // activities + UFPD bool + Eids bool + PreciseGeo bool + TID bool } // Any returns true if at least one privacy policy requires enforcement. -func (e Enforcement) Any() bool { +func (e Enforcement) AnyLegacy() bool { return e.CCPA || e.COPPA || e.GDPRGeo || e.GDPRID || e.LMT } +func (e Enforcement) AnyActivities() bool { + return e.UFPD || e.PreciseGeo || e.Eids || e.TID +} + // Apply cleans personally identifiable information from an OpenRTB bid request. -func (e Enforcement) Apply(bidRequest *openrtb2.BidRequest) { - e.apply(bidRequest, NewScrubber()) +func (e Enforcement) Apply(bidRequest *openrtb2.BidRequest, privacy config.AccountPrivacy) { + e.apply(bidRequest, NewScrubber(privacy.IPv6Config, privacy.IPv4Config)) } func (e Enforcement) apply(bidRequest *openrtb2.BidRequest, scrubber Scrubber) { - if bidRequest != nil && e.Any() { - bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) - bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy()) + if bidRequest != nil { + if e.AnyActivities() { + bidRequest = scrubber.ScrubRequest(bidRequest, e) + } + if e.AnyLegacy() && !(e.UFPD && e.PreciseGeo && e.Eids) { + bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy()) + } + if e.AnyLegacy() && !(e.UFPD && e.PreciseGeo) { + bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) + } } } @@ -38,21 +58,16 @@ func (e Enforcement) getDeviceIDScrubStrategy() ScrubStrategyDeviceID { func (e Enforcement) getIPv4ScrubStrategy() ScrubStrategyIPV4 { if e.COPPA || e.GDPRGeo || e.CCPA || e.LMT { - return ScrubStrategyIPV4Lowest8 + return ScrubStrategyIPV4Subnet } return ScrubStrategyIPV4None } func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 { - if e.COPPA { - return ScrubStrategyIPV6Lowest32 - } - - if e.GDPRGeo || e.CCPA || e.LMT { - return ScrubStrategyIPV6Lowest16 + if e.GDPRGeo || e.CCPA || e.LMT || e.COPPA { + return ScrubStrategyIPV6Subnet } - return ScrubStrategyIPV6None } @@ -69,17 +84,9 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { } func (e Enforcement) getUserScrubStrategy() ScrubStrategyUser { - if e.COPPA { + if e.COPPA || e.CCPA || e.LMT || e.GDPRID { return ScrubStrategyUserIDAndDemographic } - if e.CCPA || e.LMT { - return ScrubStrategyUserID - } - - if e.GDPRID { - return ScrubStrategyUserID - } - return ScrubStrategyUserNone } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 5902a913665..a97779eb903 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/mock" ) -func TestAny(t *testing.T) { +func TestAnyLegacy(t *testing.T) { testCases := []struct { enforcement Enforcement expected bool @@ -50,12 +50,12 @@ func TestAny(t *testing.T) { } for _, test := range testCases { - result := test.enforcement.Any() + result := test.enforcement.AnyLegacy() assert.Equal(t, test.expected, result, test.description) } } -func TestApply(t *testing.T) { +func TestApplyGDPR(t *testing.T) { testCases := []struct { description string enforcement Enforcement @@ -76,8 +76,8 @@ func TestApply(t *testing.T) { LMT: true, }, expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, @@ -92,10 +92,10 @@ func TestApply(t *testing.T) { LMT: false, }, expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserID, + expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { @@ -108,8 +108,8 @@ func TestApply(t *testing.T) { LMT: false, }, expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, @@ -124,10 +124,10 @@ func TestApply(t *testing.T) { LMT: false, }, expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserID, + expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { @@ -143,7 +143,7 @@ func TestApply(t *testing.T) { expectedDeviceIPv4: ScrubStrategyIPV4None, expectedDeviceIPv6: ScrubStrategyIPV6None, expectedDeviceGeo: ScrubStrategyGeoNone, - expectedUser: ScrubStrategyUserID, + expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoNone, }, { @@ -156,8 +156,8 @@ func TestApply(t *testing.T) { LMT: false, }, expectedDeviceID: ScrubStrategyDeviceIDNone, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserNone, expectedUserGeo: ScrubStrategyGeoReducedPrecision, @@ -172,10 +172,10 @@ func TestApply(t *testing.T) { LMT: true, }, expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserID, + expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { @@ -188,8 +188,8 @@ func TestApply(t *testing.T) { LMT: false, }, expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceIPv4: ScrubStrategyIPV4Subnet, + expectedDeviceIPv6: ScrubStrategyIPV6Subnet, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, @@ -216,6 +216,130 @@ func TestApply(t *testing.T) { } } +func TestApplyToggle(t *testing.T) { + testCases := []struct { + description string + enforcement Enforcement + expectedScrubRequestExecuted bool + expectedScrubUserExecuted bool + expectedScrubDeviceExecuted bool + }{ + { + description: "All enforced - only ScrubRequest execution expected", + enforcement: Enforcement{ + CCPA: true, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: true, + UFPD: true, + Eids: true, + PreciseGeo: true, + TID: true, + }, + expectedScrubRequestExecuted: true, + expectedScrubUserExecuted: false, + expectedScrubDeviceExecuted: false, + }, + { + description: "All Legacy and no activities - ScrubUser and ScrubDevice execution expected", + enforcement: Enforcement{ + CCPA: true, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: true, + UFPD: false, + Eids: false, + PreciseGeo: false, + TID: false, + }, + expectedScrubRequestExecuted: false, + expectedScrubUserExecuted: true, + expectedScrubDeviceExecuted: true, + }, + { + description: "Some Legacy and some activities - ScrubRequest, ScrubUser and ScrubDevice execution expected", + enforcement: Enforcement{ + CCPA: true, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: true, + UFPD: true, + Eids: false, + PreciseGeo: false, + TID: false, + }, + expectedScrubRequestExecuted: true, + expectedScrubUserExecuted: true, + expectedScrubDeviceExecuted: true, + }, + { + description: "Some Legacy and some activities - ScrubRequest execution expected", + enforcement: Enforcement{ + CCPA: true, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: true, + UFPD: true, + Eids: true, + PreciseGeo: true, + TID: false, + }, + expectedScrubRequestExecuted: true, + expectedScrubUserExecuted: false, + expectedScrubDeviceExecuted: false, + }, + { + description: "Some Legacy and some activities overlap - ScrubRequest and ScrubUser execution expected", + enforcement: Enforcement{ + CCPA: true, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: true, + UFPD: true, + Eids: false, + PreciseGeo: true, + TID: false, + }, + expectedScrubRequestExecuted: true, + expectedScrubUserExecuted: true, + expectedScrubDeviceExecuted: false, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + req := &openrtb2.BidRequest{ + Device: &openrtb2.Device{}, + User: &openrtb2.User{}, + } + replacedDevice := &openrtb2.Device{} + replacedUser := &openrtb2.User{} + + m := &mockScrubber{} + + if test.expectedScrubRequestExecuted { + m.On("ScrubRequest", req, test.enforcement).Return(req).Once() + } + if test.expectedScrubUserExecuted { + m.On("ScrubUser", req.User, ScrubStrategyUserIDAndDemographic, ScrubStrategyGeoFull).Return(replacedUser).Once() + } + if test.expectedScrubDeviceExecuted { + m.On("ScrubDevice", req.Device, ScrubStrategyDeviceIDAll, ScrubStrategyIPV4Subnet, ScrubStrategyIPV6Subnet, ScrubStrategyGeoFull).Return(replacedDevice).Once() + } + + test.enforcement.apply(req, m) + + m.AssertExpectations(t) + + }) + } +} + func TestApplyNoneApplicable(t *testing.T) { req := &openrtb2.BidRequest{} @@ -227,6 +351,11 @@ func TestApplyNoneApplicable(t *testing.T) { GDPRGeo: false, GDPRID: false, LMT: false, + + UFPD: false, + PreciseGeo: false, + TID: false, + Eids: false, } enforcement.apply(req, m) @@ -248,6 +377,11 @@ type mockScrubber struct { mock.Mock } +func (m *mockScrubber) ScrubRequest(bidRequest *openrtb2.BidRequest, enforcement Enforcement) *openrtb2.BidRequest { + args := m.Called(bidRequest, enforcement) + return args.Get(0).(*openrtb2.BidRequest) +} + func (m *mockScrubber) ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device { args := m.Called(device, id, ipv4, ipv6, geo) return args.Get(0).(*openrtb2.Device) diff --git a/privacy/gpp/gpp.go b/privacy/gpp/gpp.go index fe3507b6b0d..350004062e8 100644 --- a/privacy/gpp/gpp.go +++ b/privacy/gpp/gpp.go @@ -1,8 +1,37 @@ package gpp +import ( + gpplib "github.com/prebid/go-gpp" + gppConstants "github.com/prebid/go-gpp/constants" +) + // Policy represents the GPP privacy string container. // Currently just a placeholder until more expansive support is made. type Policy struct { Consent string RawSID string // This is the CSV format ("2,6") that the IAB recommends for passing the SID(s) on a query string. } + +// IsSIDInList returns true if the 'sid' value is found in the gppSIDs array. Its logic is used in more than +// one place in our codebase, therefore it was decided to make it its own function. +func IsSIDInList(gppSIDs []int8, sid gppConstants.SectionID) bool { + for _, id := range gppSIDs { + if id == int8(sid) { + return true + } + } + return false +} + +// IndexOfSID returns a zero or non-negative integer that represents the position of +// the 'sid' value in the 'gpp.SectionTypes' array. If the 'sid' value is not found, +// returns -1. This logic is used in more than one place in our codebase, therefore +// it was decided to make it its own function. +func IndexOfSID(gpp gpplib.GppContainer, sid gppConstants.SectionID) int { + for i, id := range gpp.SectionTypes { + if id == sid { + return i + } + } + return -1 +} diff --git a/privacy/gpp/gpp_test.go b/privacy/gpp/gpp_test.go new file mode 100644 index 00000000000..c76c6598b3b --- /dev/null +++ b/privacy/gpp/gpp_test.go @@ -0,0 +1,107 @@ +package gpp + +import ( + "testing" + + gpplib "github.com/prebid/go-gpp" + gppConstants "github.com/prebid/go-gpp/constants" + "github.com/stretchr/testify/assert" +) + +func TestIsSIDInList(t *testing.T) { + type testInput struct { + gppSIDs []int8 + sid gppConstants.SectionID + } + testCases := []struct { + desc string + in testInput + expected bool + }{ + { + desc: "nil_gppSID_array,_expect_false", + in: testInput{ + gppSIDs: nil, + sid: gppConstants.SectionTCFEU2, + }, + expected: false, + }, + { + desc: "empty_gppSID_array, expect_false", + in: testInput{ + gppSIDs: []int8{}, + sid: gppConstants.SectionTCFEU2, + }, + expected: false, + }, + { + desc: "SID_not_found_in_gppSID_array,_expect_false", + in: testInput{ + gppSIDs: []int8{int8(8), int8(9)}, + sid: gppConstants.SectionTCFEU2, + }, + expected: false, + }, + { + desc: "SID_found_in_gppSID_array,_expect_true", + in: testInput{ + gppSIDs: []int8{int8(2)}, + sid: gppConstants.SectionTCFEU2, + }, + expected: true, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { assert.Equal(t, tc.expected, IsSIDInList(tc.in.gppSIDs, tc.in.sid)) }) + } +} + +func TestIndexOfSID(t *testing.T) { + type testInput struct { + gpp gpplib.GppContainer + sid gppConstants.SectionID + } + testCases := []struct { + desc string + in testInput + expected int + }{ + { + desc: "Empty_SectionTypes_array,_expect_-1_out", + in: testInput{ + gpp: gpplib.GppContainer{}, + sid: gppConstants.SectionTCFEU2, + }, + expected: -1, + }, + { + desc: "SID_not_found_in_SectionTypes_array,_expect_-1_out", + in: testInput{ + gpp: gpplib.GppContainer{ + Version: 1, + SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, + }, + sid: gppConstants.SectionTCFEU2, + }, + expected: -1, + }, + { + desc: "SID_matches_an_element_in_SectionTypes_array,_expect_index_1_out", + in: testInput{ + gpp: gpplib.GppContainer{ + Version: 1, + SectionTypes: []gppConstants.SectionID{ + gppConstants.SectionUSPV1, + gppConstants.SectionTCFEU2, + gppConstants.SectionUSPCA, + }, + }, + sid: gppConstants.SectionTCFEU2, + }, + expected: 1, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { assert.Equal(t, tc.expected, IndexOfSID(tc.in.gpp, tc.in.sid)) }) + } +} diff --git a/privacy/lmt/ios.go b/privacy/lmt/ios.go index 9e1847b3df7..0b308a9ce32 100644 --- a/privacy/lmt/ios.go +++ b/privacy/lmt/ios.go @@ -56,7 +56,7 @@ func modifyForIOS142OrGreater(req *openrtb2.BidRequest) { switch *atts { case openrtb_ext.IOSAppTrackingStatusNotDetermined: - req.Device.Lmt = &int8Zero + req.Device.Lmt = &int8One case openrtb_ext.IOSAppTrackingStatusRestricted: req.Device.Lmt = &int8One case openrtb_ext.IOSAppTrackingStatusDenied: diff --git a/privacy/lmt/ios_test.go b/privacy/lmt/ios_test.go index 3387ff8e0a3..2a679bfbd99 100644 --- a/privacy/lmt/ios_test.go +++ b/privacy/lmt/ios_test.go @@ -54,7 +54,7 @@ func TestModifyForIOS(t *testing.T) { App: &openrtb2.App{}, Device: &openrtb2.Device{Ext: json.RawMessage(`{"atts":0}`), OS: "iOS", OSV: "14.2", IFA: "", Lmt: nil}, }, - expectedLMT: openrtb2.Int8Ptr(0), + expectedLMT: openrtb2.Int8Ptr(1), }, { description: "14.2", @@ -272,7 +272,7 @@ func TestModifyForIOS142OrGreater(t *testing.T) { { description: "Not Determined", givenDevice: openrtb2.Device{Ext: json.RawMessage(`{"atts":0}`), Lmt: nil}, - expectedLMT: openrtb2.Int8Ptr(0), + expectedLMT: openrtb2.Int8Ptr(1), }, { description: "Restricted", diff --git a/privacy/enforcer.go b/privacy/policyenforcer.go similarity index 94% rename from privacy/enforcer.go rename to privacy/policyenforcer.go index 0d5ecad5309..e70c0d3d190 100644 --- a/privacy/enforcer.go +++ b/privacy/policyenforcer.go @@ -1,5 +1,7 @@ package privacy +// NOTE: Reanme this package. Will eventually replace in its entirety with Activites. + // PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy. type PolicyEnforcer interface { // CanEnforce returns true when policy information is specifically provided by the publisher. diff --git a/privacy/enforcer_test.go b/privacy/policyenforcer_test.go similarity index 100% rename from privacy/enforcer_test.go rename to privacy/policyenforcer_test.go diff --git a/privacy/rules.go b/privacy/rules.go new file mode 100644 index 00000000000..502e10efbad --- /dev/null +++ b/privacy/rules.go @@ -0,0 +1,59 @@ +package privacy + +import ( + "strings" +) + +type Rule interface { + Evaluate(target Component) ActivityResult +} + +type ComponentEnforcementRule struct { + result ActivityResult + componentName []Component + componentType []string +} + +func (r ComponentEnforcementRule) Evaluate(target Component) ActivityResult { + if matched := evaluateComponentName(target, r.componentName); !matched { + return ActivityAbstain + } + + if matched := evaluateComponentType(target, r.componentType); !matched { + return ActivityAbstain + } + + return r.result +} + +func evaluateComponentName(target Component, componentNames []Component) bool { + // no clauses are considered a match + if len(componentNames) == 0 { + return true + } + + // if there are clauses, at least one needs to match + for _, n := range componentNames { + if n.Matches(target) { + return true + } + } + + return false +} + +func evaluateComponentType(target Component, componentTypes []string) bool { + // no clauses are considered a match + if len(componentTypes) == 0 { + return true + } + + // if there are clauses, at least one needs to match + for _, s := range componentTypes { + if strings.EqualFold(s, target.Type) { + return true + } + } + + return false +} diff --git a/privacy/rules_test.go b/privacy/rules_test.go new file mode 100644 index 00000000000..7e80c9cee95 --- /dev/null +++ b/privacy/rules_test.go @@ -0,0 +1,97 @@ +package privacy + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComponentEnforcementRuleEvaluate(t *testing.T) { + testCases := []struct { + name string + componentRule ComponentEnforcementRule + target Component + activityResult ActivityResult + }{ + { + name: "activity_is_allowed", + componentRule: ComponentEnforcementRule{ + result: ActivityAllow, + componentName: []Component{ + {Type: "bidder", Name: "bidderA"}, + }, + componentType: []string{"bidder"}, + }, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: ActivityAllow, + }, + { + name: "activity_is_not_allowed", + componentRule: ComponentEnforcementRule{ + result: ActivityDeny, + componentName: []Component{ + {Type: "bidder", Name: "bidderA"}, + }, + componentType: []string{"bidder"}, + }, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: ActivityDeny, + }, + { + name: "abstain_both_clauses_do_not_match", + componentRule: ComponentEnforcementRule{ + result: ActivityAllow, + componentName: []Component{ + {Type: "bidder", Name: "bidderA"}, + }, + componentType: []string{"bidder"}, + }, + target: Component{Type: "bidder", Name: "bidderB"}, + activityResult: ActivityAbstain, + }, + { + name: "activity_is_not_allowed_componentName_only", + componentRule: ComponentEnforcementRule{ + result: ActivityAllow, + componentName: []Component{ + {Type: "bidder", Name: "bidderA"}, + }, + }, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: ActivityAllow, + }, + { + name: "activity_is_allowed_componentType_only", + componentRule: ComponentEnforcementRule{ + result: ActivityAllow, + componentType: []string{"bidder"}, + }, + target: Component{Type: "bidder", Name: "bidderB"}, + activityResult: ActivityAllow, + }, + { + name: "no-conditions-allow", + componentRule: ComponentEnforcementRule{ + result: ActivityAllow, + }, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: ActivityAllow, + }, + { + name: "no-conditions-deny", + componentRule: ComponentEnforcementRule{ + result: ActivityDeny, + }, + target: Component{Type: "bidder", Name: "bidderA"}, + activityResult: ActivityDeny, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actualResult := test.componentRule.Evaluate(test.target) + assert.Equal(t, test.activityResult, actualResult) + + }) + } +} diff --git a/privacy/scrubber.go b/privacy/scrubber.go index c2ea7d63441..4cc90c825df 100644 --- a/privacy/scrubber.go +++ b/privacy/scrubber.go @@ -2,7 +2,10 @@ package privacy import ( "encoding/json" - "strings" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/util/iputil" + "github.com/prebid/prebid-server/util/ptrutil" + "net" "github.com/prebid/openrtb/v19/openrtb2" ) @@ -14,8 +17,8 @@ const ( // ScrubStrategyIPV4None does not remove any part of an IPV4 address. ScrubStrategyIPV4None ScrubStrategyIPV4 = iota - // ScrubStrategyIPV4Lowest8 zeroes out the last 8 bits of an IPV4 address. - ScrubStrategyIPV4Lowest8 + // ScrubStrategyIPV4Subnet zeroes out the last 8 bits of an IPV4 address. + ScrubStrategyIPV4Subnet ) // ScrubStrategyIPV6 defines the approach to scrub PII from an IPV6 address. @@ -25,11 +28,8 @@ const ( // ScrubStrategyIPV6None does not remove any part of an IPV6 address. ScrubStrategyIPV6None ScrubStrategyIPV6 = iota - // ScrubStrategyIPV6Lowest16 zeroes out the last 16 bits of an IPV6 address. - ScrubStrategyIPV6Lowest16 - - // ScrubStrategyIPV6Lowest32 zeroes out the last 32 bits of an IPV6 address. - ScrubStrategyIPV6Lowest32 + // ScrubStrategyIPV6Subnet zeroes out the last 16 bits of an IPV6 sub net address. + ScrubStrategyIPV6Subnet ) // ScrubStrategyGeo defines the approach to scrub PII from geographical data. @@ -55,9 +55,6 @@ const ( // ScrubStrategyUserIDAndDemographic removes the user's buyer id, exchange id year of birth, and gender. ScrubStrategyUserIDAndDemographic - - // ScrubStrategyUserID removes the user's buyer id. - ScrubStrategyUserID ) // ScrubStrategyDeviceID defines the approach to remove hardware id and device id data. @@ -73,18 +70,121 @@ const ( // Scrubber removes PII from parts of an OpenRTB request. type Scrubber interface { + ScrubRequest(bidRequest *openrtb2.BidRequest, enforcement Enforcement) *openrtb2.BidRequest ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device ScrubUser(user *openrtb2.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb2.User } -type scrubber struct{} +type scrubber struct { + ipV6 config.IPv6 + ipV4 config.IPv4 +} // NewScrubber returns an OpenRTB scrubber. -func NewScrubber() Scrubber { - return scrubber{} +func NewScrubber(ipV6 config.IPv6, ipV4 config.IPv4) Scrubber { + return scrubber{ + ipV6: ipV6, + ipV4: ipV4, + } } -func (scrubber) ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device { +func (s scrubber) ScrubRequest(bidRequest *openrtb2.BidRequest, enforcement Enforcement) *openrtb2.BidRequest { + var userExtParsed map[string]json.RawMessage + userExtModified := false + + var userCopy *openrtb2.User + userCopy = ptrutil.Clone(bidRequest.User) + + var deviceCopy *openrtb2.Device + deviceCopy = ptrutil.Clone(bidRequest.Device) + + if userCopy != nil && (enforcement.UFPD || enforcement.Eids) { + if len(userCopy.Ext) != 0 { + json.Unmarshal(userCopy.Ext, &userExtParsed) + } + } + + if enforcement.UFPD { + // transmitUfpd covers user.ext.data, user.data, user.id, user.buyeruid, user.yob, user.gender, user.keywords, user.kwarray + // and device.{ifa, macsha1, macmd5, dpidsha1, dpidmd5, didsha1, didmd5} + if deviceCopy != nil { + deviceCopy.DIDMD5 = "" + deviceCopy.DIDSHA1 = "" + deviceCopy.DPIDMD5 = "" + deviceCopy.DPIDSHA1 = "" + deviceCopy.IFA = "" + deviceCopy.MACMD5 = "" + deviceCopy.MACSHA1 = "" + } + if userCopy != nil { + userCopy.Data = nil + userCopy.ID = "" + userCopy.BuyerUID = "" + userCopy.Yob = 0 + userCopy.Gender = "" + userCopy.Keywords = "" + userCopy.KwArray = nil + + _, hasField := userExtParsed["data"] + if hasField { + delete(userExtParsed, "data") + userExtModified = true + } + } + } + if enforcement.Eids { + //transmitEids covers user.eids and user.ext.eids + if userCopy != nil { + userCopy.EIDs = nil + _, hasField := userExtParsed["eids"] + if hasField { + delete(userExtParsed, "eids") + userExtModified = true + } + } + } + + if userExtModified { + userExt, _ := json.Marshal(userExtParsed) + userCopy.Ext = userExt + } + + if enforcement.TID { + //remove source.tid and imp.ext.tid + if bidRequest.Source != nil { + var sourceCopy *openrtb2.Source + sourceCopy = ptrutil.Clone(bidRequest.Source) + sourceCopy.TID = "" + bidRequest.Source = sourceCopy + } + for ind, imp := range bidRequest.Imp { + impExt := scrubExtIDs(imp.Ext, "tid") + bidRequest.Imp[ind].Ext = impExt + } + } + + if enforcement.PreciseGeo { + //round user's geographic location by rounding off IP address and lat/lng data. + //this applies to both device.geo and user.geo + if userCopy != nil && userCopy.Geo != nil { + userCopy.Geo = scrubGeoPrecision(userCopy.Geo) + } + + if deviceCopy != nil { + if deviceCopy.Geo != nil { + deviceCopy.Geo = scrubGeoPrecision(deviceCopy.Geo) + } + deviceCopy.IP = scrubIP(deviceCopy.IP, s.ipV4.AnonKeepBits, iputil.IPv4BitSize) + deviceCopy.IPv6 = scrubIP(deviceCopy.IPv6, s.ipV6.AnonKeepBits, iputil.IPv6BitSize) + } + } + + bidRequest.Device = deviceCopy + bidRequest.User = userCopy + return bidRequest +} + +func (s scrubber) ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device { if device == nil { return nil } @@ -103,15 +203,13 @@ func (scrubber) ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, i } switch ipv4 { - case ScrubStrategyIPV4Lowest8: - deviceCopy.IP = scrubIPV4Lowest8(device.IP) + case ScrubStrategyIPV4Subnet: + deviceCopy.IP = scrubIP(device.IP, s.ipV4.AnonKeepBits, iputil.IPv4BitSize) } switch ipv6 { - case ScrubStrategyIPV6Lowest16: - deviceCopy.IPv6 = scrubIPV6Lowest16Bits(device.IPv6) - case ScrubStrategyIPV6Lowest32: - deviceCopy.IPv6 = scrubIPV6Lowest32Bits(device.IPv6) + case ScrubStrategyIPV6Subnet: + deviceCopy.IPv6 = scrubIP(device.IPv6, s.ipV6.AnonKeepBits, iputil.IPv6BitSize) } switch geo { @@ -131,17 +229,12 @@ func (scrubber) ScrubUser(user *openrtb2.User, strategy ScrubStrategyUser, geo S userCopy := *user - switch strategy { - case ScrubStrategyUserIDAndDemographic: + if strategy == ScrubStrategyUserIDAndDemographic { userCopy.BuyerUID = "" userCopy.ID = "" - userCopy.Ext = scrubUserExtIDs(userCopy.Ext) + userCopy.Ext = scrubExtIDs(userCopy.Ext, "eids") userCopy.Yob = 0 userCopy.Gender = "" - case ScrubStrategyUserID: - userCopy.BuyerUID = "" - userCopy.ID = "" - userCopy.Ext = scrubUserExtIDs(userCopy.Ext) } switch geo { @@ -154,44 +247,13 @@ func (scrubber) ScrubUser(user *openrtb2.User, strategy ScrubStrategyUser, geo S return &userCopy } -func scrubIPV4Lowest8(ip string) string { - i := strings.LastIndex(ip, ".") - if i == -1 { - return "" - } - - return ip[0:i] + ".0" -} - -func scrubIPV6Lowest16Bits(ip string) string { - ip = removeLowestIPV6Segment(ip) - - if ip != "" { - ip += ":0" - } - - return ip -} - -func scrubIPV6Lowest32Bits(ip string) string { - ip = removeLowestIPV6Segment(ip) - ip = removeLowestIPV6Segment(ip) - - if ip != "" { - ip += ":0:0" - } - - return ip -} - -func removeLowestIPV6Segment(ip string) string { - i := strings.LastIndex(ip, ":") - - if i == -1 { +func scrubIP(ip string, ones, bits int) string { + if ip == "" { return "" } - - return ip[0:i] + ipMask := net.CIDRMask(ones, bits) + ipMasked := net.ParseIP(ip).Mask(ipMask) + return ipMasked.String() } func scrubGeoFull(geo *openrtb2.Geo) *openrtb2.Geo { @@ -213,25 +275,25 @@ func scrubGeoPrecision(geo *openrtb2.Geo) *openrtb2.Geo { return &geoCopy } -func scrubUserExtIDs(userExt json.RawMessage) json.RawMessage { - if len(userExt) == 0 { - return userExt +func scrubExtIDs(ext json.RawMessage, fieldName string) json.RawMessage { + if len(ext) == 0 { + return ext } var userExtParsed map[string]json.RawMessage - err := json.Unmarshal(userExt, &userExtParsed) + err := json.Unmarshal(ext, &userExtParsed) if err != nil { - return userExt + return ext } - _, hasEids := userExtParsed["eids"] - if hasEids { - delete(userExtParsed, "eids") + _, hasField := userExtParsed[fieldName] + if hasField { + delete(userExtParsed, fieldName) result, err := json.Marshal(userExtParsed) if err == nil { return result } } - return userExt + return ext } diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go index d8f2fdd8429..59e593fc167 100644 --- a/privacy/scrubber_test.go +++ b/privacy/scrubber_test.go @@ -2,6 +2,7 @@ package privacy import ( "encoding/json" + "github.com/prebid/prebid-server/config" "testing" "github.com/prebid/openrtb/v19/openrtb2" @@ -9,24 +10,7 @@ import ( ) func TestScrubDevice(t *testing.T) { - device := &openrtb2.Device{ - DIDMD5: "anyDIDMD5", - DIDSHA1: "anyDIDSHA1", - DPIDMD5: "anyDPIDMD5", - DPIDSHA1: "anyDPIDSHA1", - MACSHA1: "anyMACSHA1", - MACMD5: "anyMACMD5", - IFA: "anyIFA", - IP: "1.2.3.4", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", - Geo: &openrtb2.Geo{ - Lat: 123.456, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, - } + device := getTestDevice() testCases := []struct { description string @@ -55,12 +39,12 @@ func TestScrubDevice(t *testing.T) { MACMD5: "", IFA: "", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", + IPv6: "2001:1db8:2233:4400::", Geo: &openrtb2.Geo{}, }, id: ScrubStrategyDeviceIDAll, - ipv4: ScrubStrategyIPV4Lowest8, - ipv6: ScrubStrategyIPV6Lowest32, + ipv4: ScrubStrategyIPV4Subnet, + ipv6: ScrubStrategyIPV6Subnet, geo: ScrubStrategyGeoFull, }, { @@ -74,7 +58,7 @@ func TestScrubDevice(t *testing.T) { MACMD5: "", IFA: "", IP: "1.2.3.4", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", Geo: device.Geo, }, id: ScrubStrategyDeviceIDAll, @@ -93,16 +77,16 @@ func TestScrubDevice(t *testing.T) { MACMD5: "anyMACMD5", IFA: "anyIFA", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", Geo: device.Geo, }, id: ScrubStrategyDeviceIDNone, - ipv4: ScrubStrategyIPV4Lowest8, + ipv4: ScrubStrategyIPV4Subnet, ipv6: ScrubStrategyIPV6None, geo: ScrubStrategyGeoNone, }, { - description: "Isolated - IPv6 - Lowest 16", + description: "Isolated - IPv6", expected: &openrtb2.Device{ DIDMD5: "anyDIDMD5", DIDSHA1: "anyDIDSHA1", @@ -112,31 +96,12 @@ func TestScrubDevice(t *testing.T) { MACMD5: "anyMACMD5", IFA: "anyIFA", IP: "1.2.3.4", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + IPv6: "2001:1db8:2233:4400::", Geo: device.Geo, }, id: ScrubStrategyDeviceIDNone, ipv4: ScrubStrategyIPV4None, - ipv6: ScrubStrategyIPV6Lowest16, - geo: ScrubStrategyGeoNone, - }, - { - description: "Isolated - IPv6 - Lowest 32", - expected: &openrtb2.Device{ - DIDMD5: "anyDIDMD5", - DIDSHA1: "anyDIDSHA1", - DPIDMD5: "anyDPIDMD5", - DPIDSHA1: "anyDPIDSHA1", - MACSHA1: "anyMACSHA1", - MACMD5: "anyMACMD5", - IFA: "anyIFA", - IP: "1.2.3.4", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: device.Geo, - }, - id: ScrubStrategyDeviceIDNone, - ipv4: ScrubStrategyIPV4None, - ipv6: ScrubStrategyIPV6Lowest32, + ipv6: ScrubStrategyIPV6Subnet, geo: ScrubStrategyGeoNone, }, { @@ -150,7 +115,7 @@ func TestScrubDevice(t *testing.T) { MACMD5: "anyMACMD5", IFA: "anyIFA", IP: "1.2.3.4", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", Geo: &openrtb2.Geo{ Lat: 123.46, Lon: 678.89, @@ -175,7 +140,7 @@ func TestScrubDevice(t *testing.T) { MACMD5: "anyMACMD5", IFA: "anyIFA", IP: "1.2.3.4", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", Geo: &openrtb2.Geo{}, }, id: ScrubStrategyDeviceIDNone, @@ -184,33 +149,21 @@ func TestScrubDevice(t *testing.T) { geo: ScrubStrategyGeoFull, }, } - + testIPMasking := getTestIPMasking() for _, test := range testCases { - result := NewScrubber().ScrubDevice(device, test.id, test.ipv4, test.ipv6, test.geo) + result := NewScrubber(testIPMasking.IPv6Config, testIPMasking.IPv4Config).ScrubDevice(device, test.id, test.ipv4, test.ipv6, test.geo) assert.Equal(t, test.expected, result, test.description) } } func TestScrubDeviceNil(t *testing.T) { - result := NewScrubber().ScrubDevice(nil, ScrubStrategyDeviceIDNone, ScrubStrategyIPV4None, ScrubStrategyIPV6None, ScrubStrategyGeoNone) + testIPMasking := getTestIPMasking() + result := NewScrubber(testIPMasking.IPv6Config, testIPMasking.IPv4Config).ScrubDevice(nil, ScrubStrategyDeviceIDNone, ScrubStrategyIPV4None, ScrubStrategyIPV6None, ScrubStrategyGeoNone) assert.Nil(t, result) } func TestScrubUser(t *testing.T) { - user := &openrtb2.User{ - ID: "anyID", - BuyerUID: "anyBuyerUID", - Yob: 42, - Gender: "anyGender", - Ext: json.RawMessage(`{}`), - Geo: &openrtb2.Geo{ - Lat: 123.456, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, - } + user := getTestUser() testCases := []struct { description string @@ -269,57 +222,6 @@ func TestScrubUser(t *testing.T) { scrubUser: ScrubStrategyUserIDAndDemographic, scrubGeo: ScrubStrategyGeoNone, }, - { - description: "User ID & Geo Full", - expected: &openrtb2.User{ - ID: "", - BuyerUID: "", - Yob: 42, - Gender: "anyGender", - Ext: json.RawMessage(`{}`), - Geo: &openrtb2.Geo{}, - }, - scrubUser: ScrubStrategyUserID, - scrubGeo: ScrubStrategyGeoFull, - }, - { - description: "User ID & Geo Reduced", - expected: &openrtb2.User{ - ID: "", - BuyerUID: "", - Yob: 42, - Gender: "anyGender", - Ext: json.RawMessage(`{}`), - Geo: &openrtb2.Geo{ - Lat: 123.46, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, - }, - scrubUser: ScrubStrategyUserID, - scrubGeo: ScrubStrategyGeoReducedPrecision, - }, - { - description: "User ID & Geo None", - expected: &openrtb2.User{ - ID: "", - BuyerUID: "", - Yob: 42, - Gender: "anyGender", - Ext: json.RawMessage(`{}`), - Geo: &openrtb2.Geo{ - Lat: 123.456, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, - }, - scrubUser: ScrubStrategyUserID, - scrubGeo: ScrubStrategyGeoNone, - }, { description: "User None & Geo Full", expected: &openrtb2.User{ @@ -373,132 +275,283 @@ func TestScrubUser(t *testing.T) { }, } + testIPMasking := getTestIPMasking() for _, test := range testCases { - result := NewScrubber().ScrubUser(user, test.scrubUser, test.scrubGeo) + result := NewScrubber(testIPMasking.IPv6Config, testIPMasking.IPv4Config).ScrubUser(user, test.scrubUser, test.scrubGeo) assert.Equal(t, test.expected, result, test.description) } } func TestScrubUserNil(t *testing.T) { - result := NewScrubber().ScrubUser(nil, ScrubStrategyUserNone, ScrubStrategyGeoNone) + testIPMasking := getTestIPMasking() + result := NewScrubber(testIPMasking.IPv6Config, testIPMasking.IPv4Config).ScrubUser(nil, ScrubStrategyUserNone, ScrubStrategyGeoNone) assert.Nil(t, result) } -func TestScrubIPV4(t *testing.T) { +func TestScrubRequest(t *testing.T) { + + imps := []openrtb2.Imp{ + {ID: "testId", Ext: json.RawMessage(`{"test": 1, "tid": 2}`)}, + } + source := &openrtb2.Source{ + TID: "testTid", + } + device := getTestDevice() + user := getTestUser() + user.Ext = json.RawMessage(`{"data": 1, "eids": 2}`) + user.EIDs = []openrtb2.EID{{Source: "test"}} + testCases := []struct { - IP string - cleanedIP string - description string + description string + enforcement Enforcement + userExtPresent bool + expected *openrtb2.BidRequest }{ { - IP: "0.0.0.0", - cleanedIP: "0.0.0.0", - description: "Shouldn't do anything for a 0.0.0.0 IP address", + description: "enforce transmitUFPD with user.ext", + enforcement: Enforcement{UFPD: true}, + userExtPresent: true, + expected: &openrtb2.BidRequest{ + Imp: imps, + Source: source, + User: &openrtb2.User{ + EIDs: []openrtb2.EID{{Source: "test"}}, + Geo: user.Geo, + Ext: json.RawMessage(`{"eids":2}`), + }, + Device: &openrtb2.Device{ + IP: "1.2.3.4", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", + Geo: device.Geo, + }, + }, }, { - IP: "192.127.111.134", - cleanedIP: "192.127.111.0", - description: "Should remove the lowest 8 bits", + description: "enforce transmitUFPD without user.ext", + enforcement: Enforcement{UFPD: true}, + userExtPresent: false, + expected: &openrtb2.BidRequest{ + Imp: imps, + Source: source, + User: &openrtb2.User{ + EIDs: []openrtb2.EID{{Source: "test"}}, + Geo: user.Geo, + }, + Device: &openrtb2.Device{ + IP: "1.2.3.4", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", + Geo: device.Geo, + }, + }, }, { - IP: "192.127.111.0", - cleanedIP: "192.127.111.0", - description: "Shouldn't change anything if the lowest 8 bits are already 0", + description: "enforce transmitEids", + enforcement: Enforcement{Eids: true}, + userExtPresent: true, + expected: &openrtb2.BidRequest{ + Imp: imps, + Source: source, + Device: device, + User: &openrtb2.User{ + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 42, + Gender: "anyGender", + Geo: user.Geo, + EIDs: nil, + Ext: json.RawMessage(`{"data":1}`), + }, + }, }, { - IP: "not an ip", - cleanedIP: "", - description: "Should return an empty string for a bad IP", + description: "enforce transmitTid", + enforcement: Enforcement{TID: true}, + userExtPresent: true, + expected: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "testId", Ext: json.RawMessage(`{"test":1}`)}, + }, + Source: &openrtb2.Source{ + TID: "", + }, + Device: device, + User: &openrtb2.User{ + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 42, + Gender: "anyGender", + Geo: user.Geo, + EIDs: []openrtb2.EID{{Source: "test"}}, + Ext: json.RawMessage(`{"data": 1, "eids": 2}`), + }, + }, }, { - IP: "", - cleanedIP: "", - description: "Should return an empty string for a bad IP", + description: "enforce precise Geo", + enforcement: Enforcement{PreciseGeo: true}, + userExtPresent: true, + expected: &openrtb2.BidRequest{ + Imp: imps, + Source: source, + User: &openrtb2.User{ + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 42, + Gender: "anyGender", + Geo: &openrtb2.Geo{ + Lat: 123.46, Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + EIDs: []openrtb2.EID{{Source: "test"}}, + Ext: json.RawMessage(`{"data": 1, "eids": 2}`), + }, + Device: &openrtb2.Device{ + IFA: "anyIFA", + DIDSHA1: "anyDIDSHA1", + DIDMD5: "anyDIDMD5", + DPIDSHA1: "anyDPIDSHA1", + DPIDMD5: "anyDPIDMD5", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IP: "1.2.3.0", + IPv6: "2001:1db8:2233:4400::", + Geo: &openrtb2.Geo{ + Lat: 123.46, Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + }, }, } + testIPMasking := getTestIPMasking() for _, test := range testCases { - result := scrubIPV4Lowest8(test.IP) - assert.Equal(t, test.cleanedIP, result, test.description) + t.Run(test.description, func(t *testing.T) { + bidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "testId", Ext: json.RawMessage(`{"test": 1, "tid": 2}`)}, + }, + Source: &openrtb2.Source{ + TID: "testTid", + }, + User: getTestUser(), + Device: getTestDevice(), + } + if test.userExtPresent { + bidRequest.User.Ext = json.RawMessage(`{"data": 1, "eids": 2}`) + } else { + bidRequest.User.Ext = nil + } + bidRequest.User.EIDs = []openrtb2.EID{{Source: "test"}} + + result := NewScrubber(testIPMasking.IPv6Config, testIPMasking.IPv4Config).ScrubRequest(bidRequest, test.enforcement) + assert.Equal(t, test.expected, result, test.description) + }) } } -func TestScrubIPV6Lowest16Bits(t *testing.T) { +func TestScrubIP(t *testing.T) { testCases := []struct { - IP string - cleanedIP string - description string + IP string + cleanedIP string + bits int + maskBits int }{ { - IP: "0:0:0:0", - cleanedIP: "0:0:0:0", - description: "Shouldn't do anything for a 0:0:0:0 IP address", + IP: "0:0:0:0:0:0:0:0", + cleanedIP: "::", + bits: 128, + maskBits: 56, }, { - IP: "2001:0db8:0000:0000:0000:ff00:0042:8329", - cleanedIP: "2001:0db8:0000:0000:0000:ff00:0042:0", - description: "Should remove lowest 16 bits", + IP: "", + cleanedIP: "", + bits: 128, + maskBits: 56, }, { - IP: "2001:0db8:0000:0000:0000:ff00:0042:0", - cleanedIP: "2001:0db8:0000:0000:0000:ff00:0042:0", - description: "Shouldn't do anything if the lowest 16 bits are already 0", + IP: "1111:2222:3333:4444:5555:6666:7777:8888", + cleanedIP: "1111:2222:3333:4400::", + bits: 128, + maskBits: 56, }, { - IP: "not an ip", - cleanedIP: "", - description: "Should return an empty string for a bad IP", + IP: "1111:2222:3333:4444:5555:6666:7777:8888", + cleanedIP: "1111:2222::", + bits: 128, + maskBits: 34, }, { - IP: "", - cleanedIP: "", - description: "Should return an empty string for a bad IP", + IP: "1111:0:3333:4444:5555:6666:7777:8888", + cleanedIP: "1111:0:3333:4400::", + bits: 128, + maskBits: 56, }, - } - - for _, test := range testCases { - result := scrubIPV6Lowest16Bits(test.IP) - assert.Equal(t, test.cleanedIP, result, test.description) - } -} - -func TestScrubIPV6Lowest32Bits(t *testing.T) { - testCases := []struct { - IP string - cleanedIP string - description string - }{ { - IP: "0:0:0:0", - cleanedIP: "0:0:0:0", - description: "Shouldn't do anything for a 0:0:0:0 IP address", + IP: "1111::6666:7777:8888", + cleanedIP: "1111::", + bits: 128, + maskBits: 56, }, { - IP: "2001:0db8:0000:0000:0000:ff00:0042:8329", - cleanedIP: "2001:0db8:0000:0000:0000:ff00:0:0", - description: "Should remove lowest 32 bits", + IP: "2001:1db8:0000:0000:0000:ff00:0042:8329", + cleanedIP: "2001:1db8::ff00:0:0", + bits: 128, + maskBits: 96, }, { - IP: "2001:0db8:0000:0000:0000:ff00:0:0", - cleanedIP: "2001:0db8:0000:0000:0000:ff00:0:0", - description: "Shouldn't do anything if the lowest 32 bits are already 0", + IP: "2001:1db8:0000:0000:0000:ff00:0:0", + cleanedIP: "2001:1db8::ff00:0:0", + bits: 128, + maskBits: 96, + }, + { + IP: "2001:1db8:0000:0000:0000:ff00:0042:8329", + cleanedIP: "2001:1db8::ff00:42:0", + bits: 128, + maskBits: 112, + }, + { + IP: "2001:1db8:0000:0000:0000:ff00:0042:0", + cleanedIP: "2001:1db8::ff00:42:0", + bits: 128, + maskBits: 112, }, - { - IP: "not an ip", - cleanedIP: "", - description: "Should return an empty string for a bad IP", + IP: "127.0.0.1", + cleanedIP: "127.0.0.0", + bits: 32, + maskBits: 24, }, { - IP: "", - cleanedIP: "", - description: "Should return an empty string for a bad IP", + IP: "0.0.0.0", + cleanedIP: "0.0.0.0", + bits: 32, + maskBits: 24, + }, + { + IP: "192.127.111.134", + cleanedIP: "192.127.111.0", + bits: 32, + maskBits: 24, + }, + { + IP: "192.127.111.0", + cleanedIP: "192.127.111.0", + bits: 32, + maskBits: 24, }, } - for _, test := range testCases { - result := scrubIPV6Lowest32Bits(test.IP) - assert.Equal(t, test.cleanedIP, result, test.description) + t.Run(test.IP, func(t *testing.T) { + // bits: ipv6 - 128, ipv4 - 32 + result := scrubIP(test.IP, test.maskBits, test.bits) + assert.Equal(t, test.cleanedIP, result) + }) } } @@ -623,7 +676,56 @@ func TestScrubUserExtIDs(t *testing.T) { } for _, test := range testCases { - result := scrubUserExtIDs(test.userExt) + result := scrubExtIDs(test.userExt, "eids") assert.Equal(t, test.expected, result, test.description) } } + +func getTestUser() *openrtb2.User { + return &openrtb2.User{ + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 42, + Gender: "anyGender", + Ext: json.RawMessage(`{}`), + Geo: &openrtb2.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + } +} + +func getTestDevice() *openrtb2.Device { + return &openrtb2.Device{ + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", + IPv6: "2001:1db8:2233:4455:6677:ff00:0042:8329", + Geo: &openrtb2.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + } +} + +func getTestIPMasking() config.AccountPrivacy { + return config.AccountPrivacy{ + IPv6Config: config.IPv6{ + 54, + }, + IPv4Config: config.IPv4{ + 24, + }, + } +} diff --git a/router/router.go b/router/router.go index 23cecff51a3..f7270445ff4 100644 --- a/router/router.go +++ b/router/router.go @@ -22,6 +22,7 @@ import ( "github.com/prebid/prebid-server/experiment/adscert" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/hooks" + "github.com/prebid/prebid-server/macros" "github.com/prebid/prebid-server/metrics" metricsConf "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/modules" @@ -54,6 +55,10 @@ import ( // This function stores the file contents in memory, and should not be used on large directories. // If the root directory, or any of the files in it, cannot be read, then the program will exit. func NewJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.BidderParamValidator, aliases map[string]string) httprouter.Handle { + return newJsonDirectoryServer(schemaDirectory, validator, aliases, openrtb_ext.GetAliasBidderToParent()) +} + +func newJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.BidderParamValidator, aliases map[string]string, yamlAliases map[openrtb_ext.BidderName]openrtb_ext.BidderName) httprouter.Handle { // Slurp the files into memory first, since they're small and it minimizes request latency. files, err := os.ReadDir(schemaDirectory) if err != nil { @@ -72,6 +77,11 @@ func NewJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.Bidder data[bidder] = json.RawMessage(validator.Schema(bidderName)) } + // Add in any aliases + for aliasName, parentBidder := range yamlAliases { + data[string(aliasName)] = json.RawMessage(validator.Schema(parentBidder)) + } + // Add in any default aliases for aliasName, bidderName := range aliases { bidderData, ok := data[bidderName] @@ -189,7 +199,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R } activeBidders := exchange.GetActiveBidders(cfg.BidderInfos) - disabledBidders := exchange.GetDisabledBiddersErrorMessages(cfg.BidderInfos) + disabledBidders := exchange.GetDisabledBidderWarningMessages(cfg.BidderInfos) defaultAliases, defReqJSON := readDefaultRequest(cfg.DefReqConfig) if err := validateDefaultAliases(defaultAliases); err != nil { @@ -213,20 +223,22 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R glog.Fatalf("Failed to create ads cert signer: %v", err) } + tmaxAdjustments := exchange.ProcessTMaxAdjustments(cfg.TmaxAdjustments) planBuilder := hooks.NewExecutionPlanBuilder(cfg.Hooks, repo) - theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, tcf2CfgBuilder, rateConvertor, categoriesFetcher, adsCertSigner) + macroReplacer := macros.NewStringIndexBasedReplacer() + theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer) var uuidGenerator uuidutil.UUIDRandomGenerator - openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder) + openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments) if err != nil { glog.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err) } - ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder) + ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments) if err != nil { glog.Fatalf("Failed to create the amp endpoint handler. %v", err) } - videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, cacheClient) + videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, cacheClient, tmaxAdjustments) if err != nil { glog.Fatalf("Failed to create the video endpoint handler. %v", err) } diff --git a/router/router_test.go b/router/router_test.go index 2b4ff7fd3e7..41c2a724c91 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -37,8 +37,9 @@ func ensureHasKey(t *testing.T, data map[string]json.RawMessage, key string) { } func TestNewJsonDirectoryServer(t *testing.T) { - alias := map[string]string{"aliastest": "appnexus"} - handler := NewJsonDirectoryServer("../static/bidder-params", &testValidator{}, alias) + defaultAlias := map[string]string{"aliastest": "appnexus"} + yamlAlias := map[openrtb_ext.BidderName]openrtb_ext.BidderName{openrtb_ext.BidderName("alias"): openrtb_ext.BidderName("parentAlias")} + handler := newJsonDirectoryServer("../static/bidder-params", &testValidator{}, defaultAlias, yamlAlias) recorder := httptest.NewRecorder() request, _ := http.NewRequest("GET", "/whatever", nil) handler(recorder, request, nil) @@ -59,6 +60,7 @@ func TestNewJsonDirectoryServer(t *testing.T) { } ensureHasKey(t, data, "aliastest") + ensureHasKey(t, data, "alias") } func TestCheckSupportedUserSyncEndpoints(t *testing.T) { diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 00000000000..d5f33e517e4 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +die() { echo -e "$@" 1>&2 ; exit 1; } + +AUTOFMT=true +while getopts 'f:' OPTION; do + case "$OPTION" in + f) + AUTOFMT="$OPTARG" + ;; + esac +done + +# Build a list of all the top-level directories in the project. +for DIRECTORY in */ ; do + GOGLOB="$GOGLOB ${DIRECTORY%/}" +done +GOGLOB="${GOGLOB/ docs/}" +GOGLOB="${GOGLOB/ vendor/}" + +# Check that there are no formatting issues +GOFMT_LINES=`gofmt -s -l $GOGLOB | tr '\\\\' '/' | wc -l | xargs` +if $AUTOFMT; then + # if there are files with formatting issues, they will be automatically corrected using the gofmt -w command + if [[ $GOFMT_LINES -ne 0 ]]; then + FMT_FILES=`gofmt -s -l $GOGLOB | tr '\\\\' '/' | xargs` + for FILE in $FMT_FILES; do + echo "Running: gofmt -s -w $FILE" + `gofmt -s -w $FILE` + done + fi +else + test $GOFMT_LINES -eq 0 || die "gofmt needs to be run, ${GOFMT_LINES} files have issues. Below is a list of files to review:\n`gofmt -s -l $GOGLOB`" +fi \ No newline at end of file diff --git a/server/listener.go b/server/listener.go index 065cb2deaa2..43917ac0a05 100644 --- a/server/listener.go +++ b/server/listener.go @@ -38,7 +38,7 @@ func (l *monitorableConnection) Close() error { return err } -func (ln *monitorableListener) Accept() (c net.Conn, err error) { +func (ln *monitorableListener) Accept() (net.Conn, error) { tc, err := ln.Listener.Accept() if err != nil { glog.Errorf("Error accepting connection: %v", err) diff --git a/server/server.go b/server/server.go index ce77fdc811a..9282f0fcf15 100644 --- a/server/server.go +++ b/server/server.go @@ -20,7 +20,7 @@ import ( // Listen blocks forever, serving PBS requests on the given port. This will block forever, until the process is shut down. func Listen(cfg *config.Configuration, handler http.Handler, adminHandler http.Handler, metrics *metricsconfig.DetailedMetricsEngine) (err error) { - stopSignals := make(chan os.Signal) + stopSignals := make(chan os.Signal, 1) signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT) // Run the servers. Fan any process-stopper signals out to each server for graceful shutdowns. @@ -91,10 +91,7 @@ func newAdminServer(cfg *config.Configuration, handler http.Handler) *http.Serve } func newMainServer(cfg *config.Configuration, handler http.Handler) *http.Server { - var serverHandler = handler - if cfg.EnableGzip { - serverHandler = gziphandler.GzipHandler(handler) - } + serverHandler := getCompressionEnabledHandler(handler, cfg.Compression.Response) return &http.Server{ Addr: cfg.Host + ":" + strconv.Itoa(cfg.Port), @@ -106,10 +103,7 @@ func newMainServer(cfg *config.Configuration, handler http.Handler) *http.Server } func newSocketServer(cfg *config.Configuration, handler http.Handler) *http.Server { - var serverHandler = handler - if cfg.EnableGzip { - serverHandler = gziphandler.GzipHandler(handler) - } + serverHandler := getCompressionEnabledHandler(handler, cfg.Compression.Response) return &http.Server{ Addr: cfg.UnixSocketName, @@ -119,6 +113,13 @@ func newSocketServer(cfg *config.Configuration, handler http.Handler) *http.Serv } } +func getCompressionEnabledHandler(h http.Handler, compressionInfo config.CompressionInfo) http.Handler { + if compressionInfo.GZIP { + h = gziphandler.GzipHandler(h) + } + return h +} + func runServer(server *http.Server, name string, listener net.Listener) (err error) { if server == nil { err = fmt.Errorf(">> Server is a nil_ptr.") diff --git a/static/adapter/appnexus/opts.json b/static/adapter/appnexus/opts.json deleted file mode 100644 index 41ee3c8f313..00000000000 --- a/static/adapter/appnexus/opts.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "iab_categories": { - "1": "IAB20-3", - "2": "IAB18-5", - "3": "IAB10-1", - "4": "IAB2-3", - "5": "IAB19-8", - "6": "IAB22-1", - "7": "IAB18-1", - "8": "IAB12-3", - "9": "IAB5-1", - "10": "IAB4-5", - "11": "IAB13-4", - "12": "IAB8-7", - "13": "IAB9-7", - "14": "IAB7-1", - "15": "IAB20-18", - "16": "IAB10-7", - "17": "IAB19-18", - "18": "IAB13-6", - "19": "IAB18-4", - "20": "IAB1-5", - "21": "IAB1-6", - "22": "IAB3-4", - "23": "IAB19-13", - "24": "IAB22-2", - "25": "IAB3-9", - "26": "IAB17-18", - "27": "IAB19-6", - "28": "IAB1-7", - "29": "IAB9-30", - "30": "IAB20-7", - "31": "IAB20-17", - "32": "IAB7-32", - "33": "IAB16-5", - "34": "IAB19-34", - "35": "IAB11-5", - "36": "IAB12-3", - "37": "IAB11-4", - "38": "IAB12-3", - "39": "IAB9-30", - "41": "IAB7-44", - "42": "IAB7-1", - "43": "IAB7-30", - "50": "IAB19-30", - "51": "IAB17-12", - "52": "IAB19-30", - "53": "IAB3-1", - "55": "IAB13-2", - "56": "IAB19-30", - "57": "IAB19-30", - "58": "IAB7-39", - "59": "IAB22-1", - "60": "IAB7-39", - "61": "IAB21-3", - "62": "IAB5-1", - "63": "IAB12-3", - "64": "IAB20-18", - "65": "IAB11-2", - "66": "IAB17-18", - "67": "IAB9-9", - "68": "IAB9-5", - "69": "IAB7-44", - "71": "IAB22-3", - "73": "IAB19-30", - "74": "IAB8-5", - "78": "IAB22-1", - "85": "IAB12-2", - "86": "IAB22-3", - "87": "IAB11-3", - "112": "IAB7-32", - "113": "IAB7-32", - "114": "IAB7-32", - "115": "IAB7-32", - "118": "IAB9-5", - "119": "IAB9-5", - "120": "IAB9-5", - "121": "IAB9-5", - "122": "IAB9-5", - "123": "IAB9-5", - "124": "IAB9-5", - "125": "IAB9-5", - "126": "IAB9-5", - "127": "IAB22-1", - "132": "IAB1-2", - "133": "IAB19-30", - "137": "IAB3-9", - "138": "IAB19-3", - "140": "IAB2-3", - "141": "IAB2-1", - "142": "IAB2-3", - "143": "IAB17-13", - "166": "IAB11-4", - "175": "IAB3-1", - "176": "IAB13-4", - "182": "IAB8-9", - "183": "IAB3-5" - } -} diff --git a/static/bidder-info/adkernel.yaml b/static/bidder-info/adkernel.yaml index 864ca71a088..1afbdc8d590 100644 --- a/static/bidder-info/adkernel.yaml +++ b/static/bidder-info/adkernel.yaml @@ -12,6 +12,6 @@ capabilities: - video userSync: redirect: - url: "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}" + url: "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" userMacro: "{UID}" -endpointCompression: "GZIP" \ No newline at end of file +endpointCompression: "GZIP" diff --git a/static/bidder-info/adocean.yaml b/static/bidder-info/adocean.yaml index 5001309593e..3932a7a75e6 100644 --- a/static/bidder-info/adocean.yaml +++ b/static/bidder-info/adocean.yaml @@ -1,4 +1,4 @@ -endpoint: "https://{{.Host}}" +endpoint: "https://{{.Host}}.adocean.pl" maintainer: email: "aoteam@gemius.com" gvlVendorID: 328 diff --git a/static/bidder-info/adquery.yaml b/static/bidder-info/adquery.yaml new file mode 100644 index 00000000000..98b6b0ea432 --- /dev/null +++ b/static/bidder-info/adquery.yaml @@ -0,0 +1,16 @@ +endpoint: "https://bidder.adquery.io/prebid/bid" +maintainer: + email: prebid@adquery.io +#endpointCompression: gzip # disabled because otherwise bidder responds with {data:null} +gvlVendorID: 902 +capabilities: +# app: # disabled because currently it's only a site, not an app (?) +# mediaTypes: +# - banner + site: + mediaTypes: + - banner +userSync: + redirect: + url: https://bidder.adquery.io/prebid/userSync?1=1&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&ccpa_consent={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: $UID \ No newline at end of file diff --git a/static/bidder-info/adsinteractive.yaml b/static/bidder-info/adsinteractive.yaml index e70c15b6235..095184725b8 100644 --- a/static/bidder-info/adsinteractive.yaml +++ b/static/bidder-info/adsinteractive.yaml @@ -1,6 +1,7 @@ endpoint: "http://bid.adsinteractive.com/prebid" maintainer: email: it@adsinteractive.com +gvlVendorID: 1212 modifyingVastXmlAllowed: false capabilities: app: @@ -11,6 +12,6 @@ capabilities: - banner userSync: redirect: - url: "http://pb.adsinteractive.com/getuid?{{.RedirectURL}}" + url: "https://sync.adsinteractive.com/getuid?{{.RedirectURL}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}" userMacro: "$AUID" - \ No newline at end of file + diff --git a/static/bidder-info/adsyield.yaml b/static/bidder-info/adsyield.yaml new file mode 100644 index 00000000000..dadd9cc3d13 --- /dev/null +++ b/static/bidder-info/adsyield.yaml @@ -0,0 +1,16 @@ +endpoint: "http://ads-pbs.open-adsyield.com/openrtb/{{.PublisherID}}?host={{.Host}}" +maintainer: + email: "engineering@project-limelight.com" +capabilities: + app: + mediaTypes: + - banner + - video + - audio + - native + site: + mediaTypes: + - banner + - video + - audio + - native diff --git a/static/bidder-info/adview.yaml b/static/bidder-info/adview.yaml index 53158cf49e2..2871653ffda 100644 --- a/static/bidder-info/adview.yaml +++ b/static/bidder-info/adview.yaml @@ -2,6 +2,8 @@ endpoint: "https://bid.adview.com/agent/thirdAdxService/{{.AccountID}}" maintainer: email: "partner@adview.com" gvlVendorID: 1022 +endpointCompression: gzip +modifyingVastXmlAllowed: true capabilities: app: mediaTypes: diff --git a/static/bidder-info/aidem.yaml b/static/bidder-info/aidem.yaml new file mode 100644 index 00000000000..35ea898aa8a --- /dev/null +++ b/static/bidder-info/aidem.yaml @@ -0,0 +1,19 @@ +endpoint: "https://zero.aidemsrv.com/ortb/v2.6/bid/request" +maintainer: + email: prebid@aidem.com +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: https://gum.aidemsrv.com/prebid_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: "$UID" diff --git a/static/bidder-info/amx.yaml b/static/bidder-info/amx.yaml index 3ede0d0875f..7ab3cadf304 100644 --- a/static/bidder-info/amx.yaml +++ b/static/bidder-info/amx.yaml @@ -14,7 +14,12 @@ capabilities: - video - native userSync: + iframe: + url: "https://prebid.a-mo.net/isyn?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&s=pbs&cb={{.RedirectURL}}" + userMacro: "$UID" redirect: - url: "https://prebid.a-mo.net/cchain/0?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}" - userMacro: "" - + url: "https://prebid.a-mo.net/cchain/0?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&s=pbs&cb={{.RedirectURL}}" + userMacro: "$UID" +endpointCompression: GZIP +openrtb: + gpp-supported: true diff --git a/static/bidder-info/axis.yaml b/static/bidder-info/axis.yaml new file mode 100644 index 00000000000..fe04b6015fb --- /dev/null +++ b/static/bidder-info/axis.yaml @@ -0,0 +1,18 @@ +endpoint: "http://prebid.axis-marketplace.com/pbserver" +maintainer: + email: "help@axis-marketplace.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://cs.axis-marketplace.com/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/bematterfull.yaml b/static/bidder-info/bematterfull.yaml new file mode 100644 index 00000000000..f4b48ee700e --- /dev/null +++ b/static/bidder-info/bematterfull.yaml @@ -0,0 +1,19 @@ +endpoint: "http://prebid-srv.mtflll-system.live/?pid={{.SourceId}}&host={{.Host}}" +maintainer: + email: "adops@bematterfull.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + # mtflll-system.live supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect \ No newline at end of file diff --git a/static/bidder-info/bluesea.yaml b/static/bidder-info/bluesea.yaml new file mode 100644 index 00000000000..14667cafd6e --- /dev/null +++ b/static/bidder-info/bluesea.yaml @@ -0,0 +1,11 @@ +endpoint: "https://bid.bluevoox.com/rtb/prebid" +maintainer: + email: prebid@blueseasx.com +endpointCompression: gzip +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + - native + - video diff --git a/static/bidder-info/brightroll.yaml b/static/bidder-info/brightroll.yaml deleted file mode 100644 index 196344e9f25..00000000000 --- a/static/bidder-info/brightroll.yaml +++ /dev/null @@ -1,17 +0,0 @@ -endpoint: "http://east-bid.ybp.yahoo.com/bid/appnexuspbs" -maintainer: - email: "dsp-supply-prebid@verizonmedia.com" -gvlVendorID: 25 -capabilities: - app: - mediaTypes: - - banner - - video - site: - mediaTypes: - - banner - - video -userSync: - redirect: - url: "https://pr-bh.ybp.yahoo.com/sync/appnexusprebidserver/?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url={{.RedirectURL}}" - userMacro: "$UID" diff --git a/static/bidder-info/cadent_aperture_mx.yaml b/static/bidder-info/cadent_aperture_mx.yaml new file mode 100644 index 00000000000..b5c72bd9a9d --- /dev/null +++ b/static/bidder-info/cadent_aperture_mx.yaml @@ -0,0 +1,17 @@ +endpoint: "https://hb.emxdgt.com" +maintainer: + email: "contactaperturemx@cadent.tv" +gvlVendorID: 183 +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video +userSync: + iframe: + url: "https://cs.emxdgt.com/um?ssp=pbs&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}" + userMacro: "$UID" diff --git a/static/bidder-info/colossus.yaml b/static/bidder-info/colossus.yaml index c19a2326f42..3b1f9c2f878 100644 --- a/static/bidder-info/colossus.yaml +++ b/static/bidder-info/colossus.yaml @@ -1,6 +1,6 @@ endpoint: "http://colossusssp.com/?c=o&m=rtb" maintainer: - email: "support@huddledmasses.com" + email: "support@colossusmediassp.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/consumable.yaml b/static/bidder-info/consumable.yaml index e1c4fc9b986..cc290149be2 100644 --- a/static/bidder-info/consumable.yaml +++ b/static/bidder-info/consumable.yaml @@ -11,6 +11,8 @@ capabilities: - banner userSync: redirect: - url: "https://e.serverbid.com/udb/9969/match?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" + url: "https://e.serverbid.com/udb/9969/match?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: "" # consumable appends the user id to end of the redirect url and does not utilize a macro +openrtb: + gpp-supported: true diff --git a/static/bidder-info/conversant.yaml b/static/bidder-info/conversant.yaml index da003b25c83..7ffd2761fde 100644 --- a/static/bidder-info/conversant.yaml +++ b/static/bidder-info/conversant.yaml @@ -1,6 +1,6 @@ endpoint: "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25" maintainer: - email: "CNVR_PublisherIntegration@conversantmedia.com" + email: "PublisherIntegration@epsilon.com" gvlVendorID: 24 capabilities: app: @@ -13,6 +13,6 @@ capabilities: - video userSync: redirect: - url: "https://prebid-match.dotomi.com/match/bounce/current?version=1&networkId=72582&rurl={{.RedirectURL}}" + url: "https://prebid-match.dotomi.com/match/bounce/current?version=1&networkId=72582&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&rurl={{.RedirectURL}}" userMacro: "" - # conversant appends the user id to end of the redirect url and does not utilize a macro + # epsilon appends the user id to end of the redirect url and does not utilize a macro diff --git a/static/bidder-info/copper6.yaml b/static/bidder-info/copper6.yaml new file mode 100644 index 00000000000..213cbe3624d --- /dev/null +++ b/static/bidder-info/copper6.yaml @@ -0,0 +1,17 @@ +endpoint: "http://ghb.app.copper6.com/pbs/ortb" +maintainer: + email: "info@copper6.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + # Copper6 ssp supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - iframe diff --git a/static/bidder-info/criteo.yaml b/static/bidder-info/criteo.yaml index 6d6b5b61d0a..7d58c68d198 100644 --- a/static/bidder-info/criteo.yaml +++ b/static/bidder-info/criteo.yaml @@ -14,9 +14,9 @@ capabilities: userSync: # criteo supports user syncing, but requires configuration by the host. contact this # bidder directly at the email address in this file to ask about enabling user sync. - supports: - - redirect - redirect: url: "https://ssp-sync.criteo.com/user-sync/redirect?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}&profile=230" + userMacro: "${CRITEO_USER_ID}" + iframe: + url: "https://ssp-sync.criteo.com/user-sync/iframe?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}&profile=230" userMacro: "${CRITEO_USER_ID}" \ No newline at end of file diff --git a/static/bidder-info/datablocks.yaml b/static/bidder-info/datablocks.yaml index 418b0cabc1e..1f8d6e8d089 100644 --- a/static/bidder-info/datablocks.yaml +++ b/static/bidder-info/datablocks.yaml @@ -1,4 +1,4 @@ -endpoint: "http://{{.Host}}/openrtb2?sid={{.SourceId}}" +endpoint: "http://pbserver.dblks.net/openrtb2?sid={{.SourceId}}" maintainer: email: "prebid@datablocks.net" capabilities: diff --git a/static/bidder-info/emtv.yaml b/static/bidder-info/emtv.yaml new file mode 100644 index 00000000000..6c9b3a11d9b --- /dev/null +++ b/static/bidder-info/emtv.yaml @@ -0,0 +1,19 @@ +endpoint: "https://us-east-ep.engagemedia.tv/pserver" +maintainer: + email: "support@engagemedia.tv" +capabilities: + site: + mediaTypes: + - banner + - video + - native + + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://cs.engagemedia.tv/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/emx_digital.yaml b/static/bidder-info/emx_digital.yaml index 0d8aa7f3a6d..b5c72bd9a9d 100644 --- a/static/bidder-info/emx_digital.yaml +++ b/static/bidder-info/emx_digital.yaml @@ -1,6 +1,6 @@ endpoint: "https://hb.emxdgt.com" maintainer: - email: "adops@emxdigital.com" + email: "contactaperturemx@cadent.tv" gvlVendorID: 183 capabilities: site: diff --git a/static/bidder-info/epsilon.yaml b/static/bidder-info/epsilon.yaml new file mode 100644 index 00000000000..856d82a6f3f --- /dev/null +++ b/static/bidder-info/epsilon.yaml @@ -0,0 +1,18 @@ +endpoint: "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25" +maintainer: + email: "PublisherIntegration@epsilon.com" +gvlVendorID: 24 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + redirect: + url: "https://prebid-match.dotomi.com/match/bounce/current?version=1&networkId=72582&rurl={{.RedirectURL}}" + userMacro: "" + # epsilon/conversant appends the user id to end of the redirect url and does not utilize a macro diff --git a/static/bidder-info/evtech.yaml b/static/bidder-info/evtech.yaml index 4cc75cd3da6..4277a5c46c8 100644 --- a/static/bidder-info/evtech.yaml +++ b/static/bidder-info/evtech.yaml @@ -14,3 +14,10 @@ capabilities: - video - audio - native +userSync: + iframe: + url: https://tracker.direct.e-volution.ai/sync.html?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: "{PLL_USER_ID}" + redirect: + url: https://tracker.direct.e-volution.ai/sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: "{PLL_USER_ID}" diff --git a/static/bidder-info/flipp.yaml b/static/bidder-info/flipp.yaml new file mode 100644 index 00000000000..431c1345794 --- /dev/null +++ b/static/bidder-info/flipp.yaml @@ -0,0 +1,7 @@ +endpoint: "https://cdn-gateflipp.flippback.com/flyer-locator-service/prebid_campaigns" +maintainer: + email: prebid@flipp.com +capabilities: + site: + mediaTypes: + - banner diff --git a/static/bidder-info/freewheel-ssp.yaml b/static/bidder-info/freewheel-ssp.yaml index f013445a820..0c0a11edfce 100644 --- a/static/bidder-info/freewheel-ssp.yaml +++ b/static/bidder-info/freewheel-ssp.yaml @@ -12,5 +12,5 @@ capabilities: - video userSync: iframe: - url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}" + url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" userMacro: "{viewerid}" diff --git a/static/bidder-info/freewheelssp.yaml b/static/bidder-info/freewheelssp.yaml index 5cef0de13b3..8c9286cbbc0 100644 --- a/static/bidder-info/freewheelssp.yaml +++ b/static/bidder-info/freewheelssp.yaml @@ -12,5 +12,5 @@ capabilities: - video userSync: iframe: - url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}" + url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" userMacro: "{viewerid}" \ No newline at end of file diff --git a/static/bidder-info/frvradn.yaml b/static/bidder-info/frvradn.yaml new file mode 100644 index 00000000000..b5b2e7f215b --- /dev/null +++ b/static/bidder-info/frvradn.yaml @@ -0,0 +1,18 @@ +endpoint: "https://fran.frvr.com/api/v1/openrtb" +maintainer: + email: "info@frvr.com" +gvlVendorID: 1107 +modifyingVastXmlAllowed: false +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + iframe: + url: "https://fran.frvr.com/api/v1/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect_uri={{.RedirectURL}}" + userMacro: "{{UID}}" \ No newline at end of file diff --git a/static/bidder-info/gothamads.yaml b/static/bidder-info/gothamads.yaml new file mode 100644 index 00000000000..c9f4e66b484 --- /dev/null +++ b/static/bidder-info/gothamads.yaml @@ -0,0 +1,14 @@ +endpoint: "http://us-e-node1.gothamads.com/?pass={{.AccountID}}" +maintainer: + email: "support@gothamads.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-info/groupm.yaml b/static/bidder-info/groupm.yaml deleted file mode 100644 index 189527d5471..00000000000 --- a/static/bidder-info/groupm.yaml +++ /dev/null @@ -1,20 +0,0 @@ -endpoint: "https://hbopenbid.pubmatic.com/translator?source=prebid-server" -maintainer: - email: "header-bidding@pubmatic.com" -gvlVendorID: 98 -capabilities: - app: - mediaTypes: - - banner - - video - site: - mediaTypes: - - banner - - video -userSync: - iframe: - url: "https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&predirect={{.RedirectURL}}" - userMacro: "" - # groupm appends the user id to end of the redirect url and does not utilize a macro - - diff --git a/static/bidder-info/huaweiads.yaml b/static/bidder-info/huaweiads.yaml index 8fe35b4e012..fc36ff953bd 100644 --- a/static/bidder-info/huaweiads.yaml +++ b/static/bidder-info/huaweiads.yaml @@ -1,7 +1,7 @@ endpoint: "https://acd.op.hicloud.com/ppsadx/getResult" disabled: true maintainer: - email: hwads@huawei.com + email: prebid@huawei.com gvlVendorID: 856 modifyingVastXmlAllowed: true capabilities: diff --git a/static/bidder-info/imds.yaml b/static/bidder-info/imds.yaml index 3f27c766df3..491a5bd0ac6 100644 --- a/static/bidder-info/imds.yaml +++ b/static/bidder-info/imds.yaml @@ -1,4 +1,4 @@ -endpoint: "http://{{.Host}}.technoratimedia.com/openrtb/bids/{{.Host}}" +endpoint: "https://pbs.technoratimedia.com/openrtb/bids/{{.AccountID}}?src={{.SourceId}}&adapter=imds" maintainer: email: "eng-demand@imds.tv" capabilities: @@ -13,8 +13,10 @@ capabilities: userSync: supportCors: true iframe: - url: "https://ad-cdn.technoratimedia.com/html/usersync.html?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}" + url: "https://ad-cdn.technoratimedia.com/html/usersync.html?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gppsid={{.GPPSID}}&cb={{.RedirectURL}}" userMacro: "[USER_ID]" redirect: - url: "https://sync.technoratimedia.com/services?srv=cs&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}" + url: "https://sync.technoratimedia.com/services?srv=cs&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gppsid={{.GPPSID}}&cb={{.RedirectURL}}" userMacro: "[USER_ID]" +openrtb: + gpp-supported: true diff --git a/static/bidder-info/inmobi.yaml b/static/bidder-info/inmobi.yaml index ad3562de64b..72bdd21d974 100644 --- a/static/bidder-info/inmobi.yaml +++ b/static/bidder-info/inmobi.yaml @@ -1,6 +1,6 @@ endpoint: "https://api.w.inmobi.com/showad/openrtb/bidder/prebid" maintainer: - email: "technology-irv@inmobi.com" + email: "prebid-support@inmobi.com" gvlVendorID: 333 capabilities: app: diff --git a/static/bidder-info/ix.yaml b/static/bidder-info/ix.yaml index 9ac7dba32dc..4ab520ad81c 100644 --- a/static/bidder-info/ix.yaml +++ b/static/bidder-info/ix.yaml @@ -17,8 +17,10 @@ capabilities: - audio userSync: redirect: - url: "https://ssum.casalemedia.com/usermatchredir?s=194962&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}" + url: "https://ssum.casalemedia.com/usermatchredir?s=194962&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gppsid={{.GPPSID}}&cb={{.RedirectURL}}" userMacro: "" iframe: - url: "https://ssum-sec.casalemedia.com/usermatch?s=184674&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}" + url: "https://ssum-sec.casalemedia.com/usermatch?s=184674&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gppsid={{.GPPSID}}&cb={{.RedirectURL}}" # ix appends the user id to end of the redirect url and does not utilize a macro +openrtb: + gpp-supported: true diff --git a/static/bidder-info/liftoff.yaml b/static/bidder-info/liftoff.yaml new file mode 100644 index 00000000000..577439dbb97 --- /dev/null +++ b/static/bidder-info/liftoff.yaml @@ -0,0 +1,9 @@ +endpoint: "https://rtb.ads.vungle.com/bid/t/c770f32" +maintainer: + email: platform-ssp@liftoff.io +endpointCompression: gzip +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - video diff --git a/static/bidder-info/lm_kiviads.yaml b/static/bidder-info/lm_kiviads.yaml new file mode 100644 index 00000000000..dcb252a45d9 --- /dev/null +++ b/static/bidder-info/lm_kiviads.yaml @@ -0,0 +1,19 @@ +endpoint: "http://pbs.kiviads.live/?pid={{.SourceId}}&host={{.Host}}" +maintainer: + email: "prebid@kiviads.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + # lmkiviads supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect \ No newline at end of file diff --git a/static/bidder-info/lunamedia.yaml b/static/bidder-info/lunamedia.yaml index 79e19a4393f..ef34143eb40 100644 --- a/static/bidder-info/lunamedia.yaml +++ b/static/bidder-info/lunamedia.yaml @@ -1,4 +1,4 @@ -endpoint: "http://api.lunamedia.io/xp/get?pubid={{.PublisherID}}" +endpoint: "http://rtb.lunamedia.live/?pid={{.PublisherID}}" maintainer: email: "josh@lunamedia.io" capabilities: @@ -6,13 +6,11 @@ capabilities: mediaTypes: - banner - video - app: mediaTypes: - banner - video userSync: iframe: - url: "https://api.lunamedia.io/xp/user-sync?redirect={{.RedirectURL}}" - userMacro: "$UID" - + url: "https://sync.lunamedia.live/psync?t=s&e=0&cb={{.RedirectURL}}" + userMacro: "%USER_ID%" diff --git a/static/bidder-info/mediafuse.yaml b/static/bidder-info/mediafuse.yaml index b78b21e16ea..be19cc6c68a 100644 --- a/static/bidder-info/mediafuse.yaml +++ b/static/bidder-info/mediafuse.yaml @@ -12,5 +12,5 @@ capabilities: - banner - video - native -userSync: - key: "adnxs" +# userSync: +# key: "adnxs" diff --git a/static/bidder-info/mgidX.yaml b/static/bidder-info/mgidX.yaml new file mode 100644 index 00000000000..ab783beb560 --- /dev/null +++ b/static/bidder-info/mgidX.yaml @@ -0,0 +1,23 @@ +endpoint: "https://us-east-x.mgid.com/pserver" +maintainer: + email: "prebid@mgid.com" +gvlVendorID: 358 +capabilities: + site: + mediaTypes: + - banner + - video + - native + + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://cm.mgid.com/i.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&adu={{.RedirectURL}}" + userMacro: "{muidn}" + iframe: + url: "https://cm.mgid.com/i.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&adu={{.RedirectURL}}" + userMacro: "{muidn}" diff --git a/static/bidder-info/motorik.yaml b/static/bidder-info/motorik.yaml new file mode 100644 index 00000000000..6818de9d87e --- /dev/null +++ b/static/bidder-info/motorik.yaml @@ -0,0 +1,14 @@ +endpoint: "http://lb-east.motorik.io/?k={{.AccountID}}&name={{.SourceId}}" +maintainer: + email: "support@motorik.io" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-info/orbidder.yaml b/static/bidder-info/orbidder.yaml index b7891af3195..705d35a5a20 100644 --- a/static/bidder-info/orbidder.yaml +++ b/static/bidder-info/orbidder.yaml @@ -6,3 +6,7 @@ capabilities: app: mediaTypes: - banner + site: + mediaTypes: + - banner + - native diff --git a/static/bidder-info/ownadx.yaml b/static/bidder-info/ownadx.yaml new file mode 100644 index 00000000000..37567db1144 --- /dev/null +++ b/static/bidder-info/ownadx.yaml @@ -0,0 +1,12 @@ +endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.AccountID}}/{{.ZoneID}}?token={{.SourceId}}" +maintainer: + email: prebid-team@techbravo.com +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/pubmatic.yaml b/static/bidder-info/pubmatic.yaml index 9841e8992e1..0a8283b4f98 100644 --- a/static/bidder-info/pubmatic.yaml +++ b/static/bidder-info/pubmatic.yaml @@ -21,3 +21,5 @@ userSync: redirect: url: "https://image8.pubmatic.com/AdServer/ImgSync?p=159706&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&pu={{.RedirectURL}}" userMacro: "#PMUID" +openrtb: + gpp-supported: true diff --git a/static/bidder-info/pwbid.yaml b/static/bidder-info/pwbid.yaml index ac6872738b4..a172ff39c83 100644 --- a/static/bidder-info/pwbid.yaml +++ b/static/bidder-info/pwbid.yaml @@ -1,4 +1,4 @@ -endpoint: "https://bid.pubwise.io/prebid" +endpoint: "https://bidder.east2.pubwise.io/bid/pubwisedirect" maintainer: email: info@pubwise.io gvlVendorID: 842 @@ -17,4 +17,4 @@ userSync: # PubWise supports user syncing, but requires configuration by the host. contact this # bidder directly at the email address in this file to ask about enabling user sync. supports: - - redirect \ No newline at end of file + - redirect diff --git a/static/bidder-info/rise.yaml b/static/bidder-info/rise.yaml new file mode 100644 index 00000000000..d429e8780a7 --- /dev/null +++ b/static/bidder-info/rise.yaml @@ -0,0 +1,18 @@ +endpoint: "https://pbs.yellowblue.io/pbs" +maintainer: + email: rise-prog-dev@risecodes.com +gvlVendorID: 1043 +modifyingVastXmlAllowed: false +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + iframe: + url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: "[PBS_UID]" diff --git a/static/bidder-info/rtbhouse.yaml b/static/bidder-info/rtbhouse.yaml index 6a116448f22..ad2fbfcbc95 100644 --- a/static/bidder-info/rtbhouse.yaml +++ b/static/bidder-info/rtbhouse.yaml @@ -1,6 +1,7 @@ endpoint: "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids" maintainer: email: "prebid@rtbhouse.com" +endpointCompression: gzip gvlVendorID: 16 capabilities: site: diff --git a/static/bidder-info/screencore.yaml b/static/bidder-info/screencore.yaml new file mode 100644 index 00000000000..67777a13782 --- /dev/null +++ b/static/bidder-info/screencore.yaml @@ -0,0 +1,14 @@ +endpoint: 'http://h1.screencore.io/?kp={{.AccountID}}&kn={{.SourceId}}' +maintainer: + email: 'connect@screencore.io' +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native diff --git a/static/bidder-info/sharethrough.yaml b/static/bidder-info/sharethrough.yaml index 0a8453d516f..0b3479f56d2 100644 --- a/static/bidder-info/sharethrough.yaml +++ b/static/bidder-info/sharethrough.yaml @@ -7,10 +7,12 @@ capabilities: mediaTypes: - banner - video + - native site: mediaTypes: - banner - video + - native userSync: redirect: url: https://match.sharethrough.com/FGMrCMMc/v1?redirectUri={{.RedirectURL}} diff --git a/static/bidder-info/silverpush.yaml b/static/bidder-info/silverpush.yaml new file mode 100644 index 00000000000..19da68a2e8c --- /dev/null +++ b/static/bidder-info/silverpush.yaml @@ -0,0 +1,12 @@ +endpoint: "https://prebid.chocolateplatform.co/bidder/?identifier=prebidchoc" +maintainer: + email: "prebid@silverpush.co" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml index 4eb5f17c17c..59a734c90fd 100644 --- a/static/bidder-info/smaato.yaml +++ b/static/bidder-info/smaato.yaml @@ -16,6 +16,6 @@ capabilities: - native userSync: redirect: - url: "https://s.ad.smaato.net/c/?adExInit=p&redir={{.RedirectURL}}" + url: "https://s.ad.smaato.net/c/?adExInit=p&redir={{.RedirectURL}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}" userMacro: "$UID" diff --git a/static/bidder-info/smartadserver.yaml b/static/bidder-info/smartadserver.yaml index 48e88d94efa..d861ed7f521 100644 --- a/static/bidder-info/smartadserver.yaml +++ b/static/bidder-info/smartadserver.yaml @@ -1,6 +1,6 @@ endpoint: "https://ssb-global.smartadserver.com" maintainer: - email: "support@smartadserver.com" + email: "supply-partner-integration@equativ.com" gvlVendorID: 45 capabilities: app: @@ -16,4 +16,4 @@ capabilities: userSync: redirect: url: "https://ssbsync-global.smartadserver.com/api/sync?callerId=5&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri={{.RedirectURL}}" - userMacro: "[ssb_sync_pid]" \ No newline at end of file + userMacro: "[ssb_sync_pid]" diff --git a/static/bidder-info/sovrn.yaml b/static/bidder-info/sovrn.yaml index 06dadcf9a05..62d74152b0b 100644 --- a/static/bidder-info/sovrn.yaml +++ b/static/bidder-info/sovrn.yaml @@ -17,6 +17,5 @@ userSync: url: "https://ap.lijit.com/pixel?redir={{.RedirectURL}}" userMacro: "$UID" iframe: - url: "https://ap.lijit.com/beacon/prebid-server/?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url={{.RedirectURL}}" + url: "https://ap.lijit.com/beacon/prebid-server/?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&url={{.RedirectURL}}" userMacro: "$UID" - \ No newline at end of file diff --git a/static/bidder-info/synacormedia.yaml b/static/bidder-info/synacormedia.yaml index c19d7674d18..2a796ae839f 100644 --- a/static/bidder-info/synacormedia.yaml +++ b/static/bidder-info/synacormedia.yaml @@ -1,5 +1,5 @@ # DEPRECATED: Use imds bidder instead -endpoint: "http://{{.Host}}.technoratimedia.com/openrtb/bids/{{.Host}}" +endpoint: "https://pbs.technoratimedia.com/openrtb/bids/{{.AccountID}}?src={{.SourceId}}&adapter=synacormedia" maintainer: email: "eng-demand@imds.tv" capabilities: @@ -12,4 +12,4 @@ capabilities: - banner - video userSync: - key: "imds" \ No newline at end of file + key: "imds" diff --git a/static/bidder-info/taboola.yaml b/static/bidder-info/taboola.yaml index 348165a992c..436f746959a 100644 --- a/static/bidder-info/taboola.yaml +++ b/static/bidder-info/taboola.yaml @@ -1,4 +1,4 @@ -endpoint: "https://{{.MediaType}}.bidder.taboola.com/OpenRTB/PS/auction/{{.GvlID}}/{{.PublisherID}}" +endpoint: "https://{{.MediaType}}.bidder.taboola.com/OpenRTB/PS/auction?exchange={{.GvlID}}&publisher={{.PublisherID}}" maintainer: email: ps-team@taboola.com gvlVendorID: 42 diff --git a/static/bidder-info/tpmn.yaml b/static/bidder-info/tpmn.yaml new file mode 100644 index 00000000000..8d88c451da6 --- /dev/null +++ b/static/bidder-info/tpmn.yaml @@ -0,0 +1,20 @@ +endpoint: "https://gat.tpmn.io/ortb/pbs_bidder" +maintainer: + email: "prebid@tpmn.io" +modifyingVastXmlAllowed: true +endpointCompression: GZIP +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://gat.tpmn.io/sync/redirect?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" + userMacro: "$UID" \ No newline at end of file diff --git a/static/bidder-info/triplelift.yaml b/static/bidder-info/triplelift.yaml index 45811c0c868..605bcc71e6e 100644 --- a/static/bidder-info/triplelift.yaml +++ b/static/bidder-info/triplelift.yaml @@ -16,9 +16,11 @@ userSync: # Contact this bidder directly at the email address above to ask about enabling user sync. # iframe: - url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" + url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: $UID redirect: - url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" + url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: "$UID" -endpointCompression: "GZIP" \ No newline at end of file +endpointCompression: "GZIP" +openrtb: + gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/triplelift_native.yaml b/static/bidder-info/triplelift_native.yaml index 85ebd1a52cc..ff93b544c4c 100644 --- a/static/bidder-info/triplelift_native.yaml +++ b/static/bidder-info/triplelift_native.yaml @@ -16,8 +16,10 @@ userSync: # Contact this bidder directly at the email address above to ask about enabling user sync. # iframe: - url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" + url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: $UID redirect: - url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" - userMacro: "$UID" \ No newline at end of file + url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "$UID" +openrtb: + gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/ucfunnel.yaml b/static/bidder-info/ucfunnel.yaml index 0034b9d4650..99af7313509 100644 --- a/static/bidder-info/ucfunnel.yaml +++ b/static/bidder-info/ucfunnel.yaml @@ -13,5 +13,6 @@ capabilities: - video userSync: redirect: + # for syncing to work, please register your Prebid Server domain with the contact email above url: "https://sync.aralego.com/idsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&usprivacy={{.USPrivacy}}&redirect={{.RedirectURL}}" userMacro: "SspCookieUserId" diff --git a/static/bidder-info/vox.yaml b/static/bidder-info/vox.yaml new file mode 100644 index 00000000000..e2b5c902be6 --- /dev/null +++ b/static/bidder-info/vox.yaml @@ -0,0 +1,19 @@ +endpoint: "https://ssp.hybrid.ai/prebid/server/v1/auction" +maintainer: + email: prebid@hybrid.ai +endpointCompression: gzip +gvlVendorID: 206 +modifyingVastXmlAllowed: true +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video +userSync: + redirect: + url: "https://ssp.hybrid.ai/prebid/server/v1/userSync?consent={{.GDPRConsent}}&redirect={{.RedirectURL}}" + userMacro: $UID \ No newline at end of file diff --git a/static/bidder-info/vrtcal.yaml b/static/bidder-info/vrtcal.yaml index 0983d546fe9..2aeb292a7ed 100644 --- a/static/bidder-info/vrtcal.yaml +++ b/static/bidder-info/vrtcal.yaml @@ -7,13 +7,14 @@ capabilities: mediaTypes: - banner - video + - native site: mediaTypes: - banner - video + - native userSync: # vrtcal supports user syncing, but requires configuration by the host. contact this # bidder directly at the email address in this file to ask about enabling user sync. supports: - redirect - diff --git a/static/bidder-info/xeworks.yaml b/static/bidder-info/xeworks.yaml new file mode 100644 index 00000000000..ba3420896ae --- /dev/null +++ b/static/bidder-info/xeworks.yaml @@ -0,0 +1,19 @@ +endpoint: "http://prebid-srv.xe.works/?pid={{.SourceId}}&host={{.Host}}" +maintainer: + email: "team@xe.works" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + # xe.works supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect \ No newline at end of file diff --git a/static/bidder-info/xtrmqb.yaml b/static/bidder-info/xtrmqb.yaml new file mode 100644 index 00000000000..c1f0da3fc1f --- /dev/null +++ b/static/bidder-info/xtrmqb.yaml @@ -0,0 +1,16 @@ +endpoint: "http://ads-pbs.ortb.tech/openrtb/{{.PublisherID}}?host={{.Host}}" +maintainer: + email: "engineering@project-limelight.com" +capabilities: + app: + mediaTypes: + - banner + - video + - audio + - native + site: + mediaTypes: + - banner + - video + - audio + - native diff --git a/static/bidder-info/verizonmedia.yaml b/static/bidder-info/yahooAds.yaml similarity index 52% rename from static/bidder-info/verizonmedia.yaml rename to static/bidder-info/yahooAds.yaml index 9be15b84091..a2581387152 100644 --- a/static/bidder-info/verizonmedia.yaml +++ b/static/bidder-info/yahooAds.yaml @@ -1,16 +1,18 @@ -disabled: true +endpoint: "https://s2shb.ssp.yahoo.com/admax/bid/partners/PBS" maintainer: - email: "dsp-supply-prebid@verizonmedia.com" + email: "hb-fe-tech@yahooinc.com" gvlVendorID: 25 capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner + - video userSync: - # verizonmedia supports user syncing, but requires configuration by the host. contact this + # yahooAds supports user syncing, but requires configuration by the host. contact this # bidder directly at the email address in this file to ask about enabling user sync. supports: - redirect \ No newline at end of file diff --git a/static/bidder-info/yahooAdvertising.yaml b/static/bidder-info/yahooAdvertising.yaml new file mode 100644 index 00000000000..3798adf7ca2 --- /dev/null +++ b/static/bidder-info/yahooAdvertising.yaml @@ -0,0 +1,18 @@ +endpoint: "https://s2shb.ssp.yahoo.com/admax/bid/partners/PBS" +maintainer: + email: "hb-fe-tech@yahooinc.com" +gvlVendorID: 25 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + # yahooAdvertising supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect \ No newline at end of file diff --git a/static/bidder-info/zeta_global_ssp.yaml b/static/bidder-info/zeta_global_ssp.yaml new file mode 100644 index 00000000000..3cc2ecb5297 --- /dev/null +++ b/static/bidder-info/zeta_global_ssp.yaml @@ -0,0 +1,20 @@ +endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA&shortname=GET_SHORTNAME_FROM_ZETA +endpointCompression: gzip +maintainer: + email: DL-Zeta-SSP@zetaglobal.com +gvlVendorID: 833 +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + redirect: + url: https://ssp.disqus.com/redirectuser?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}&partner=GET_SHORTNAME_FROM_ZETA + userMacro: 'BUYERUID' + diff --git a/static/bidder-params/adocean.json b/static/bidder-params/adocean.json index 7530c64784c..a3c5cbc95d4 100644 --- a/static/bidder-params/adocean.json +++ b/static/bidder-params/adocean.json @@ -6,9 +6,14 @@ "properties": { "emiter": { "type": "string", - "description": "AdOcean emiter", + "description": "Deprecated, use emitterPrefix instead. AdOcean emiter", "pattern": ".+" }, + "emitterPrefix": { + "type": "string", + "description": "AdOcean emitter prefix", + "pattern": "^[\\w\\-]+$" + }, "masterId": { "type": "string", "description": "Master's id", @@ -20,5 +25,8 @@ "pattern": "^adocean[\\w.]+$" } }, - "required": ["emiter", "masterId", "slaveId"] + "oneOf": [ + { "required": ["emiter", "masterId", "slaveId"] }, + { "required": ["emitterPrefix", "masterId", "slaveId"] } + ] } diff --git a/static/bidder-params/adquery.json b/static/bidder-params/adquery.json new file mode 100644 index 00000000000..f2394b5e249 --- /dev/null +++ b/static/bidder-params/adquery.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adquery Adapter Params", + "description": "A schema which validates params accepted by the Adquery adapter", + "type": "object", + + "properties": { + "placementId": { + "type": "string", + "minLength": 35, + "maxLength": 45, + "description": "Placement ID" + }, + "type": { + "type": "string", + "minLength": 1, + "description": "Bid type" + } + }, + + "required": ["placementId", "type"] +} diff --git a/static/bidder-params/adsyield.json b/static/bidder-params/adsyield.json new file mode 100644 index 00000000000..c7c5308890f --- /dev/null +++ b/static/bidder-params/adsyield.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adsyield Adapter Params", + "description": "A schema which validates params accepted by the Adsyield adapter", + "type": "object", + + "properties": { + "host": { + "type": "string", + "description": "Ad network's RTB host", + "format": "hostname", + "pattern": "^.+\\..+$" + }, + "publisherId": { + "type": ["integer", "string"], + "description": "Publisher ID", + "minimum": 1, + "pattern": "^[1-9][0-9]*$" + } + }, + + "required": ["host", "publisherId"] +} diff --git a/static/bidder-params/aidem.json b/static/bidder-params/aidem.json new file mode 100644 index 00000000000..221e5ff7a92 --- /dev/null +++ b/static/bidder-params/aidem.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AIDEM Adapter Params", + "description": "A schema which validates params accepted by the AIDEM adapter", + "type": "object", + "properties": { + "siteId": { + "type": "string", + "minLength": 1, + "description": "Unique site ID" + }, + "publisherId": { + "type": "string", + "minLength": 1, + "description": "Unique publisher ID" + }, + "placementId": { + "type": "string", + "minLength": 1, + "description": "Unique publisher ttag ID" + }, + "rateLimit": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": ["siteId", "publisherId"] +} \ No newline at end of file diff --git a/static/bidder-params/appnexus.json b/static/bidder-params/appnexus.json index c3e47129b7a..6a9a5d7d45d 100644 --- a/static/bidder-params/appnexus.json +++ b/static/bidder-params/appnexus.json @@ -26,25 +26,42 @@ "description": "An ID which identifies the member selling the impression." }, "keywords": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.", - "properties": { - "key": { - "type": "string" - }, - "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": ["key"] + } + }, + { + "type": "object", + "additionalProperties": { "type": "array", - "minItems": 1, "items": { "type": "string" } } - }, - "required": ["key"] - } + } + ] }, "traffic_source_code": { "type": "string", @@ -86,6 +103,14 @@ "required": [ "w", "h"] }, "description": "Private sizes (ex: [{\"w\": 300, \"h\": 250},{...}]), experimental, may not be supported." + }, + "ext_inv_code" : { + "type": "string", + "description": "Specifies predefined value passed on the query string that can be used in reporting" + }, + "external_imp_id" : { + "type": "string", + "description": "Unique identifier of an externally generated auction" } }, diff --git a/static/bidder-params/axis.json b/static/bidder-params/axis.json new file mode 100644 index 00000000000..791f4df2c04 --- /dev/null +++ b/static/bidder-params/axis.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Axis Adapter Params", + "description": "A schema which validates params accepted by the Axis adapter", + "type": "object", + "properties": { + "integration": { + "type": "string", + "description": "Integration", + "minLength": 6, + "maxLength": 6 + }, + "token": { + "type": "string", + "description": "Token", + "minLength": 6, + "maxLength": 6 + } + }, + "required": [ + "integration", + "token" + ] +} diff --git a/static/bidder-params/bematterfull.json b/static/bidder-params/bematterfull.json new file mode 100644 index 00000000000..db3a505050c --- /dev/null +++ b/static/bidder-params/bematterfull.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bematterfull Adapter Params", + "description": "A schema which validates params accepted by the bematterfull adapter", + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "bematterfull environment", + "minLength": 1 + }, + "pid": { + "type": "string", + "description": "Uniq placement ID", + "minLength": 1 + } + }, + "required": [ + "env", + "pid" + ] +} diff --git a/static/bidder-params/bluesea.json b/static/bidder-params/bluesea.json new file mode 100644 index 00000000000..81aaea9bfca --- /dev/null +++ b/static/bidder-params/bluesea.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bluesea Adapter Params", + "description": "A schema which validates params accepted by the Bluesea adapter", + + "type": "object", + "properties": { + "pubid": { + "type": "string", + "description": "the unique partner account id", + "pattern": "^[0-9]+$" + }, + "token": { + "type": "string", + "description": "token", + "pattern": "[0-9|a-z|A-Z]{16}" + } + }, + "required": [ + "pubid", + "token" + ] +} diff --git a/static/bidder-params/brightroll.json b/static/bidder-params/brightroll.json deleted file mode 100644 index 48e48d8a36d..00000000000 --- a/static/bidder-params/brightroll.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Brightroll Adapter Params", - "description": "A schema which validates params accepted by the Brightroll adapter", - "type": "object", - "properties": { - "publisher": { - "type": "string", - "description": "Publisher Name to use." - } - }, - "required": ["publisher"] -} diff --git a/static/bidder-params/cadent_aperture_mx.json b/static/bidder-params/cadent_aperture_mx.json new file mode 100644 index 00000000000..4e3ff51fc1b --- /dev/null +++ b/static/bidder-params/cadent_aperture_mx.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Cadent Aperture MX Adapter Params", + "description": "A schema which validates params accepted by the Cadent Aperture MX adapter", + "type": "object", + "properties": { + "tagid" : { + "type": "string", + "description": "The id of an inventory target" + }, + "bidfloor": { + "type": "string", + "description": "The minimum price acceptable for a bid" + } + }, + + "required": ["tagid"] + } diff --git a/static/bidder-params/conversant.json b/static/bidder-params/conversant.json index ba9c6bd584d..883ef053a27 100644 --- a/static/bidder-params/conversant.json +++ b/static/bidder-params/conversant.json @@ -1,12 +1,12 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Conversant Adapter Params", - "description": "A schema which validates params accepted by the Conversant adapter.", + "title": "Epsilon (formerly Conversant) Adapter Params", + "description": "A schema which validates params accepted by the Epsilon (Conversant) adapter.", "type": "object", "properties": { "site_id": { "type": "string", - "description": "A Conversant specific ID which identifies the site." + "description": "An Epsilon (Conversant) specific ID which identifies the site." }, "secure": { "type": "integer", diff --git a/static/bidder-params/copper6.json b/static/bidder-params/copper6.json new file mode 100644 index 00000000000..fa4050f6c84 --- /dev/null +++ b/static/bidder-params/copper6.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Copper6 Adapter Params", + "description": "A schema which validates params accepted by the Copper6 ssp adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "aid": { + "type": "integer", + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": ["aid"] +} diff --git a/static/bidder-params/datablocks.json b/static/bidder-params/datablocks.json index 2abb986786b..59b7f94706c 100644 --- a/static/bidder-params/datablocks.json +++ b/static/bidder-params/datablocks.json @@ -9,11 +9,7 @@ "type": "integer", "minimum": 1, "description": "Website Source Id" - }, - "host": { - "type": "string", - "description": "Network Host to request from" } }, - "required": ["host", "sourceId"] + "required": ["sourceId"] } diff --git a/static/bidder-params/emtv.json b/static/bidder-params/emtv.json new file mode 100644 index 00000000000..a491b825d37 --- /dev/null +++ b/static/bidder-params/emtv.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "EMTV Adapter Params", + "description": "A schema which validates params accepted by the EMTV adapter", + "type": "object", + + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["endpointId"] } + ] + } \ No newline at end of file diff --git a/static/bidder-params/emx_digital.json b/static/bidder-params/emx_digital.json index f7516f57e63..42c243a9f4f 100644 --- a/static/bidder-params/emx_digital.json +++ b/static/bidder-params/emx_digital.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "title": "EMX Digital Adapter Params", - "description": "A schema which validates params accepted by the EMX Digital adapter", + "description": "A schema which validates params accepted by the Cadent Aperture MX adapter", "type": "object", "properties": { "tagid" : { diff --git a/static/bidder-params/epsilon.json b/static/bidder-params/epsilon.json new file mode 100644 index 00000000000..a33a68325e4 --- /dev/null +++ b/static/bidder-params/epsilon.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Epsilon Adapter Params", + "description": "A schema which validates params accepted by the Epsilon adapter.", + "type": "object", + "properties": { + "site_id": { + "type": "string", + "description": "An Epsilon specific ID which identifies the site." + }, + "secure": { + "type": "integer", + "description": "Override http/https context on ad markup." + }, + "bidfloor" : { + "type": "number", + "description": "Minimum bid price that will be considered." + }, + "tag_id": { + "type": "string", + "description": "Identifies specific ad placement." + }, + "position": { + "type": "integer", + "description": "Ad position on screen." + }, + "mimes": { + "type": "array", + "description": "Array of content MIME types. For videos only.", + "items": { + "type": "string" + } + }, + "maxduration": { + "type": "integer", + "description": "Maximum duration in seconds. For videos only." + }, + "api": { + "type": "array", + "description": "Array of supported API frameworks. For videos only.", + "items": { + "type": "integer" + } + }, + "protocols": { + "type": "array", + "description": "Array of supported video protocols. For videos only.", + "items": { + "type": "integer" + } + } + }, + "required": ["site_id"] +} \ No newline at end of file diff --git a/static/bidder-params/flipp.json b/static/bidder-params/flipp.json new file mode 100644 index 00000000000..b2952eda444 --- /dev/null +++ b/static/bidder-params/flipp.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Flipp Adapter Params", + "description": "A schema which validates params accepted by the Flipp adapter", + "type": "object", + "properties": { + "publisherNameIdentifier": { + "type": "string", + "minLength": 1, + "description": "Publisher Name Identifier" + }, + "creativeType": { + "type": "string", + "enum": ["NativeX", "DTX"] + }, + "siteId": { + "type": "integer" + }, + "zoneIds": { + "type": "array", + "items": { + "type": "integer" + } + }, + "userKey": { + "type": "string", + "format": "uuid" + }, + "ip": { + "type": "string" + }, + "options": { + "type": "object", + "properties": { + "startCompact": { + "type": "boolean" + }, + "dwellExpand": { + "type": "boolean" + }, + "contentCode": { + "type": "string" + } + } + } + }, + "required": [ + "publisherNameIdentifier", + "siteId", + "creativeType" + ] +} \ No newline at end of file diff --git a/static/bidder-params/freewheel-ssp.json b/static/bidder-params/freewheel-ssp.json index 103aed03198..23375f7603c 100644 --- a/static/bidder-params/freewheel-ssp.json +++ b/static/bidder-params/freewheel-ssp.json @@ -6,7 +6,7 @@ "properties": { "zoneId": { - "type": "integer", + "type": ["integer", "string"], "description": "Zone ID" } }, diff --git a/static/bidder-params/freewheelssp.json b/static/bidder-params/freewheelssp.json index 6f93501a3e1..ced3c28daf3 100644 --- a/static/bidder-params/freewheelssp.json +++ b/static/bidder-params/freewheelssp.json @@ -6,7 +6,7 @@ "properties": { "zoneId": { - "type": "integer", + "type": ["integer", "string"], "description": "Zone ID" } }, diff --git a/static/bidder-params/frvradn.json b/static/bidder-params/frvradn.json new file mode 100644 index 00000000000..09edaba10cc --- /dev/null +++ b/static/bidder-params/frvradn.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "FRVR Ad Network Adapter Params", + "description": "A schema which validates params accepted by the FRVR Ad Network adapter", + "type": "object", + "properties": { + "publisher_id": { + "type": ["string"], + "description": "Publisher ID" + }, + "ad_unit_id": { + "type": ["string"], + "description": "Ad Unit ID" + } + }, + "required": ["publisher_id", "ad_unit_id"] +} diff --git a/static/bidder-params/gothamads.json b/static/bidder-params/gothamads.json new file mode 100644 index 00000000000..030d43eb311 --- /dev/null +++ b/static/bidder-params/gothamads.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Gothamads Adapter Params", + "description": "A schema which validates params accepted by the Gothamads adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + } + }, + "required": [ + "accountId" + ] +} \ No newline at end of file diff --git a/static/bidder-params/groupm.json b/static/bidder-params/groupm.json deleted file mode 100644 index cfebd2adb19..00000000000 --- a/static/bidder-params/groupm.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Groupm Adapter Params", - "description": "A schema which validates params accepted by the Groupm adapter", - "type": "object", - "properties": { - "publisherId": { - "type": "string", - "description": "An ID which identifies the publisher" - }, - "adSlot": { - "type": "string", - "description": "An ID which identifies the ad slot" - }, - "pmzoneid": { - "type": "string", - "description": "Comma separated zone id. Used im deal targeting & site section targeting. e.g drama,sport" - }, - "dctr": { - "type": "string", - "description": "Deals Custom Targeting, pipe separated key-value pairs e.g key1=V1,V2,V3|key2=v1|key3=v3,v5" - }, - "wrapper": { - "type": "object", - "description": "Specifies Groupm openwrap configuration for a publisher", - "properties": { - "profile": { - "type": "integer", - "description": "An ID which identifies the openwrap profile of publisher" - }, - "version": { - "type": "integer", - "description": "An ID which identifies version of the openwrap profile" - } - }, - "required": ["profile"] - }, - "keywords": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["publisherId"] -} diff --git a/static/bidder-params/ix.json b/static/bidder-params/ix.json index a7a5cb7308a..172690cca32 100644 --- a/static/bidder-params/ix.json +++ b/static/bidder-params/ix.json @@ -27,6 +27,11 @@ "minItems": 2, "maxItems": 2, "description": "An array of two integer containing the dimension" + }, + "sid": { + "type": "string", + "minLength": 1, + "description": "Slot ID" } }, "oneOf": [ diff --git a/static/bidder-params/kargo.json b/static/bidder-params/kargo.json index 15c11526b7c..314eb63f3e7 100644 --- a/static/bidder-params/kargo.json +++ b/static/bidder-params/kargo.json @@ -3,13 +3,18 @@ "title": "Kargo Adapter Params", "description": "A schema which validates params accepted by the Kargo adapter", "type": "object", - "properties": { - "adSlotID": { - "type": "string", - "description": "An ID which identifies the adslot placement. Equivalent to the id of target inventory, ad unit code, or placement id" - } + "placementId": { + "type": "string", + "description": "An ID which identifies the adslot placement. Equivalent to the id of target inventory, ad unit code, or placement id" }, - - "required": ["adSlotID"] + "adSlotID": { + "type": "string", + "description": "[Deprecated: Use `placementId`] An ID which identifies the adslot placement. Equivalent to the id of target inventory, ad unit code, or placement id" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["adSlotID"] } + ] } diff --git a/static/bidder-params/liftoff.json b/static/bidder-params/liftoff.json new file mode 100644 index 00000000000..32aa7f89a53 --- /dev/null +++ b/static/bidder-params/liftoff.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Liftoff Adapter Params", + "description": "A schema which validates params accepted by the Liftoff adapter", + "type": "object", + "properties": { + "app_store_id": { + "type": "string", + "minLength": 1, + "description": "Pub App Store ID" + }, + "placement_reference_id": { + "type": "string", + "minLength": 1, + "description": "Placement Reference ID" + } + }, + "required": [ + "app_store_id", + "placement_reference_id" + ] +} \ No newline at end of file diff --git a/static/bidder-params/lm_kiviads.json b/static/bidder-params/lm_kiviads.json new file mode 100644 index 00000000000..901a1a8e81c --- /dev/null +++ b/static/bidder-params/lm_kiviads.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Lm_Kiviads Adapter Params", + "description": "A schema which validates params accepted by the lmkiviads adapter", + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "kivi environment", + "minLength": 1 + }, + "pid": { + "type": "string", + "description": "Unique placement ID", + "minLength": 1 + } + }, + "required": [ + "env", + "pid" + ] +} diff --git a/static/bidder-params/mgidX.json b/static/bidder-params/mgidX.json new file mode 100644 index 00000000000..7b24582d1ad --- /dev/null +++ b/static/bidder-params/mgidX.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MGIDX Adapter Params", + "description": "A schema which validates params accepted by the MGIDX adapter", + "type": "object", + + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["endpointId"] } + ] + } \ No newline at end of file diff --git a/static/bidder-params/motorik.json b/static/bidder-params/motorik.json new file mode 100644 index 00000000000..261e9c1e498 --- /dev/null +++ b/static/bidder-params/motorik.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Motorik Adapter Params", + "description": "A schema which validates params accepted by the Motorik adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "placementId": { + "type": "string", + "description": "Placement id", + "minLength": 1 + } + }, + "required": ["accountId", "placementId"] + } \ No newline at end of file diff --git a/static/bidder-params/ownadx.json b/static/bidder-params/ownadx.json new file mode 100644 index 00000000000..f529e74cb01 --- /dev/null +++ b/static/bidder-params/ownadx.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "OwnAdx Adapter Params", + "description": "A schema which validates params accepted by the OwnAdx adapter", + "type": "object", + + "properties": { + "sspId": { + "type": "string", + "description": "Ssp ID" + }, + "seatId": { + "type": "string", + "description": "Seat ID" + }, + "tokenId": { + "type": "string", + "description": "Token ID" + } + }, + + "oneOf": [ + { "required": ["sspId"] }, + { "required": ["feedId"] }, + { "required": ["token"] } + ] +} diff --git a/static/bidder-params/rise.json b/static/bidder-params/rise.json new file mode 100644 index 00000000000..c5344b7ab0f --- /dev/null +++ b/static/bidder-params/rise.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rise Adapter Params", + "description": "A schema which validates params accepted by the Rise adapter", + "type": "object", + "properties": { + "org": { + "type": "string", + "description": "The organization ID." + }, + "publisher_id": { + "type": "string", + "description": "Deprecated, use org instead." + } + }, + "oneOf": [ + { "required": ["org"] }, + { "required": ["publisher_id"] } + ] +} diff --git a/static/bidder-params/rtbhouse.json b/static/bidder-params/rtbhouse.json index 00732bedd2f..727f51bcc30 100644 --- a/static/bidder-params/rtbhouse.json +++ b/static/bidder-params/rtbhouse.json @@ -7,6 +7,18 @@ "publisherId": { "type": "string", "description": "The publisher’s ID provided by RTB House" + }, + "region": { + "type": "string", + "description": "The publisher’s assigned datacenter region" + }, + "bidfloor": { + "type": "number", + "description": "(optional) Minimum bid price in CPM that will be considered" + }, + "channel": { + "type": "string", + "description": "Inventory channel identifier, limited to 50 characters" } }, "required": ["publisherId"] diff --git a/static/bidder-params/sa_lunamedia.json b/static/bidder-params/sa_lunamedia.json index 51ca09098e2..90aa2fe6fe7 100644 --- a/static/bidder-params/sa_lunamedia.json +++ b/static/bidder-params/sa_lunamedia.json @@ -13,5 +13,5 @@ "enum": ["network", "publisher"] } }, - "required": ["key"] + "required": ["key", "type"] } \ No newline at end of file diff --git a/static/bidder-params/screencore.json b/static/bidder-params/screencore.json new file mode 100644 index 00000000000..0a181b20f1c --- /dev/null +++ b/static/bidder-params/screencore.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Screencore Adapter Params", + "description": "A schema which validates params accepted by the Screencore adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "placementId": { + "type": "string", + "description": "Placement id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "placementId" + ] +} \ No newline at end of file diff --git a/static/bidder-params/silverpush.json b/static/bidder-params/silverpush.json new file mode 100644 index 00000000000..36dec33bb92 --- /dev/null +++ b/static/bidder-params/silverpush.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Silverpush OpenRTB Bidder Adapter", + "description": "A schema which validates params accepted by the SilverPush adapter", + "type": "object", + + "properties": { + "publisherId": { + "type":"string", + "description": "Publisher id provided by silverpush" + }, + "bidfloor": { + "type":"number", + "description": "Minimum price in USD. bidFloor applies to a specific unit. For example, use the following value to set a $1.50 floor: 1.50." + } + }, + + "required": ["publisherId"] + } \ No newline at end of file diff --git a/static/bidder-params/tpmn.json b/static/bidder-params/tpmn.json new file mode 100644 index 00000000000..68b54373179 --- /dev/null +++ b/static/bidder-params/tpmn.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "TPMN Adapter Params", + "description": "A schema which validates params accepted by the TPMN adapter", + "type": "object", + "properties": { + "inventoryId": { + "description": "Inventory ID", + "type": "integer", + "minLength": 1 + } + }, + "required": [ + "inventoryId" + ] +} \ No newline at end of file diff --git a/static/bidder-params/vox.json b/static/bidder-params/vox.json new file mode 100644 index 00000000000..2ae65e99fc7 --- /dev/null +++ b/static/bidder-params/vox.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Vox Adapter Params", + "description": "A schema which validates params accepted by the Vox adapter", + "type": "object", + + "properties": { + "placementId": { + "type": "string", + "description": "Placement ID" + }, + "imageUrl": { + "type": "string", + "description": "An URL of image on which banner should be put(InImage Ad format)." + }, + "displaySizes": { + "type": "array", + "items": { + "type": "string", + "description": "x(Example: 123x90, 720x100)" + }, + "description": "Display banner sizes which could be shown on top of image." + } + }, + + "required": ["placementId"] +} \ No newline at end of file diff --git a/static/bidder-params/xeworks.json b/static/bidder-params/xeworks.json new file mode 100644 index 00000000000..48ab4ab2a52 --- /dev/null +++ b/static/bidder-params/xeworks.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "xe.works Adapter Params", + "description": "A schema which validates params accepted by the xe.works adapter", + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "xe.works environment", + "minLength": 1 + }, + "pid": { + "type": "string", + "description": "Uniq placement ID", + "minLength": 1 + } + }, + "required": [ + "env", + "pid" + ] +} diff --git a/static/bidder-params/xtrmqb.json b/static/bidder-params/xtrmqb.json new file mode 100644 index 00000000000..59e711e9ad3 --- /dev/null +++ b/static/bidder-params/xtrmqb.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "XTRM QB Adapter Params", + "description": "A schema which validates params accepted by the XTRM QB adapter", + "type": "object", + + "properties": { + "host": { + "type": "string", + "description": "Ad network's RTB host", + "format": "hostname", + "pattern": "^.+\\..+$" + }, + "publisherId": { + "type": ["integer", "string"], + "description": "Publisher ID", + "minimum": 1, + "pattern": "^[1-9][0-9]*$" + } + }, + + "required": ["host", "publisherId"] +} diff --git a/static/bidder-params/verizonmedia.json b/static/bidder-params/yahooAds.json similarity index 78% rename from static/bidder-params/verizonmedia.json rename to static/bidder-params/yahooAds.json index 96649f085bd..77e7107350c 100644 --- a/static/bidder-params/verizonmedia.json +++ b/static/bidder-params/yahooAds.json @@ -1,15 +1,17 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "VerizonMedia Adapter Params", - "description": "A schema which validates params accepted by the VerizonMedia adapter", + "title": "YahooAds Adapter Params", + "description": "A schema which validates params accepted by the YahooAds adapter", "type": "object", "properties": { "dcn": { "type": "string", + "minLength": 1, "description": "Site ID provided by One Mobile" }, "pos": { "type": "string", + "minLength": 1, "description": "Placement ID" } }, diff --git a/static/bidder-params/yahooAdvertising.json b/static/bidder-params/yahooAdvertising.json new file mode 100644 index 00000000000..4778f0778c7 --- /dev/null +++ b/static/bidder-params/yahooAdvertising.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "YahooAdvertising Adapter Params", + "description": "A schema which validates params accepted by the YahooAdvertising adapter", + "type": "object", + "properties": { + "dcn": { + "type": "string", + "minLength": 1, + "description": "Site ID provided by One Mobile" + }, + "pos": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + } + }, + "required": ["dcn", "pos"] +} diff --git a/static/bidder-params/zeta_global_ssp.json b/static/bidder-params/zeta_global_ssp.json new file mode 100644 index 00000000000..acfaa9c1988 --- /dev/null +++ b/static/bidder-params/zeta_global_ssp.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Zeta Global SSP Adapter Params", + "description": "A schema which validates params accepted by the Zeta Global SSP adapter", + + "type": "object", + "properties": {} +} diff --git a/stored_requests/backends/db_provider/mysql_dbprovider.go b/stored_requests/backends/db_provider/mysql_dbprovider.go index 9e1b2e3ea3b..6301a119c45 100644 --- a/stored_requests/backends/db_provider/mysql_dbprovider.go +++ b/stored_requests/backends/db_provider/mysql_dbprovider.go @@ -8,7 +8,7 @@ import ( "database/sql" "errors" "fmt" - "io/ioutil" + "os" "regexp" "sort" "strconv" @@ -106,7 +106,7 @@ func (provider *MySqlDbProvider) ConnString() (string, error) { func setupTLSConfig(provider *MySqlDbProvider) error { rootCertPool := x509.NewCertPool() - pem, err := ioutil.ReadFile(provider.cfg.TLS.RootCert) + pem, err := os.ReadFile(provider.cfg.TLS.RootCert) if err != nil { return err } diff --git a/stored_responses/stored_responses.go b/stored_responses/stored_responses.go index b24ead2d1d9..19b010fb12d 100644 --- a/stored_responses/stored_responses.go +++ b/stored_responses/stored_responses.go @@ -146,8 +146,9 @@ func ProcessStoredResponses(ctx context.Context, requestJson []byte, storedRespF } bidderImpIdReplaceImp := flipMap(impBidderReplaceImp) - impIdToStoredResp, impBidderToStoredBidResponse := buildStoredResponsesMaps(storedResponses, impBidderToStoredBidResponseId, impIdToRespId) - return impIdToStoredResp, impBidderToStoredBidResponse, bidderImpIdReplaceImp, nil + impIdToStoredResp, impBidderToStoredBidResponse, errs := buildStoredResponsesMaps(storedResponses, impBidderToStoredBidResponseId, impIdToRespId) + + return impIdToStoredResp, impBidderToStoredBidResponse, bidderImpIdReplaceImp, errs } return nil, nil, nil, nil } @@ -166,24 +167,33 @@ func flipMap(impBidderReplaceImpId ImpBidderReplaceImpID) BidderImpReplaceImpID return flippedMap } -func buildStoredResponsesMaps(storedResponses StoredResponseIdToStoredResponse, impBidderToStoredBidResponseId ImpBiddersWithBidResponseIDs, impIdToRespId ImpsWithAuctionResponseIDs) (ImpsWithBidResponses, ImpBidderStoredResp) { +func buildStoredResponsesMaps(storedResponses StoredResponseIdToStoredResponse, impBidderToStoredBidResponseId ImpBiddersWithBidResponseIDs, impIdToRespId ImpsWithAuctionResponseIDs) (ImpsWithBidResponses, ImpBidderStoredResp, []error) { + var errs []error //imp id to stored resp body impIdToStoredResp := ImpsWithBidResponses{} //stored bid responses: imp id to bidder to stored response body impBidderToStoredBidResponse := ImpBidderStoredResp{} for impId, respId := range impIdToRespId { - impIdToStoredResp[impId] = storedResponses[respId] + if len(storedResponses[respId]) == 0 { + errs = append(errs, fmt.Errorf("failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s", impId, respId)) + } else { + impIdToStoredResp[impId] = storedResponses[respId] + } } for impId, bidderStoredResp := range impBidderToStoredBidResponseId { bidderStoredResponses := StoredResponseIdToStoredResponse{} for bidderName, id := range bidderStoredResp { - bidderStoredResponses[bidderName] = storedResponses[id] + if len(storedResponses[id]) == 0 { + errs = append(errs, fmt.Errorf("failed to fetch stored bid response for impId = %s, bidder = %s and storedBidResponse id = %s", impId, bidderName, id)) + } else { + bidderStoredResponses[bidderName] = storedResponses[id] + } } impBidderToStoredBidResponse[impId] = bidderStoredResponses } - return impIdToStoredResp, impBidderToStoredBidResponse + return impIdToStoredResp, impBidderToStoredBidResponse, errs } // parseImpInfo parses the request JSON and returns the impressions with their unmarshalled imp.ext.prebid diff --git a/stored_responses/stored_responses_test.go b/stored_responses/stored_responses_test.go index 6c4960fef24..c4ddea278a7 100644 --- a/stored_responses/stored_responses_test.go +++ b/stored_responses/stored_responses_test.go @@ -590,6 +590,213 @@ func TestProcessStoredAuctionAndBidResponses(t *testing.T) { } +func TestProcessStoredResponsesNotFoundResponse(t *testing.T) { + bidderMap := map[string]openrtb_ext.BidderName{"bidderA": "bidderA", "bidderB": "bidderB"} + bidStoredResp1 := json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "bidderA"}]`) + bidStoredResp2 := json.RawMessage(`[{"bid": [{"id": "bid_id2"],"seat": "bidderB"}]`) + mockStoredResponses := map[string]json.RawMessage{ + "1": bidStoredResp1, + "2": bidStoredResp2, + "3": nil, + "4": nil, + } + fetcher := &mockStoredBidResponseFetcher{mockStoredResponses} + + testCases := []struct { + description string + requestJson []byte + expectedErrors []error + }{ + { + description: "Stored bid response with nil data, one bidder one imp", + requestJson: []byte(`{"imp": [ + { + "id": "imp-id1", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedbidresponse": [ + {"bidder":"bidderB", "id": "3"} + ] + } + } + } + ]}`), + expectedErrors: []error{ + errors.New("failed to fetch stored bid response for impId = imp-id1, bidder = bidderB and storedBidResponse id = 3"), + }, + }, + { + description: "Stored bid response with nil data, one bidder, two imps, one with correct stored response", + requestJson: []byte(`{"imp": [ + { + "id": "imp-id1", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedbidresponse": [ + {"bidder":"bidderB", "id": "1"} + ] + } + } + }, + { + "id": "imp-id2", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedbidresponse": [ + {"bidder":"bidderB", "id": "3"} + ] + } + } + } + ]}`), + expectedErrors: []error{ + errors.New("failed to fetch stored bid response for impId = imp-id2, bidder = bidderB and storedBidResponse id = 3"), + }, + }, + { + description: "Stored bid response with nil data, one bidder, two imps, both with correct stored response", + requestJson: []byte(`{"imp": [ + { + "id": "imp-id1", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedbidresponse": [ + {"bidder":"bidderB", "id": "4"} + ] + } + } + }, + { + "id": "imp-id2", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedbidresponse": [ + {"bidder":"bidderB", "id": "3"} + ] + } + } + } + ]}`), + expectedErrors: []error{ + errors.New("failed to fetch stored bid response for impId = imp-id1, bidder = bidderB and storedBidResponse id = 4"), + errors.New("failed to fetch stored bid response for impId = imp-id2, bidder = bidderB and storedBidResponse id = 3"), + }, + }, + { + description: "Stored auction response with nil data and one imp", + requestJson: []byte(`{"imp": [ + { + "id": "imp-id1", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedauctionresponse": { + "id": "4" + } + } + } + } + ]}`), + expectedErrors: []error{ + errors.New("failed to fetch stored auction response for impId = imp-id1 and storedAuctionResponse id = 4"), + }, + }, + { + description: "Stored auction response with nil data, and two imps with nil responses", + requestJson: []byte(`{"imp": [ + { + "id": "imp-id1", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedauctionresponse": { + "id": "4" + } + } + } + }, + { + "id": "imp-id2", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedauctionresponse": { + "id": "3" + } + } + } + } + ]}`), + expectedErrors: []error{ + errors.New("failed to fetch stored auction response for impId = imp-id1 and storedAuctionResponse id = 4"), + errors.New("failed to fetch stored auction response for impId = imp-id2 and storedAuctionResponse id = 3"), + }, + }, + { + description: "Stored auction response with nil data, two imps, one with nil responses", + requestJson: []byte(`{"imp": [ + { + "id": "imp-id1", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedauctionresponse": { + "id": "2" + } + } + } + }, + { + "id": "imp-id2", + "ext": { + "appnexus": { + "placementId": 123 + }, + "prebid": { + "storedauctionresponse": { + "id": "3" + } + } + } + } + ]}`), + expectedErrors: []error{ + errors.New("failed to fetch stored auction response for impId = imp-id2 and storedAuctionResponse id = 3"), + }, + }, + } + + for _, test := range testCases { + _, _, _, errorList := ProcessStoredResponses(nil, test.requestJson, fetcher, bidderMap) + for _, err := range test.expectedErrors { + assert.Contains(t, errorList, err, "incorrect errors returned: %s", test.description) + } + } +} + func TestFlipMap(t *testing.T) { testCases := []struct { description string diff --git a/usersync/chooser.go b/usersync/chooser.go index 97fa1471b7e..3f478049066 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -85,6 +85,9 @@ const ( // StatusDuplicate specifies the bidder is a duplicate or shared a syncer key with another bidder choice. StatusDuplicate + + // StatusBlockedByPrivacy specifies a bidder sync url is not allowed by privacy activities + StatusBlockedByPrivacy ) // Privacy determines which privacy policies will be enforced for a user sync request. @@ -92,6 +95,7 @@ type Privacy interface { GDPRAllowsHostCookie() bool GDPRAllowsBidderSync(bidder string) bool CCPAAllowsBidderSync(bidder string) bool + ActivityAllowsUserSync(bidder string) bool } // standardChooser implements the user syncer algorithm per official Prebid specification. @@ -151,6 +155,11 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{} return nil, BidderEvaluation{Status: StatusAlreadySynced, Bidder: bidder, SyncerKey: syncer.Key()} } + userSyncActivityAllowed := privacy.ActivityAllowsUserSync(bidder) + if !userSyncActivityAllowed { + return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()} + } + if !privacy.GDPRAllowsBidderSync(bidder) { return nil, BidderEvaluation{Status: StatusBlockedByGDPR, Bidder: bidder, SyncerKey: syncer.Key()} } diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go index 3b820b99f24..adc217ae8f5 100644 --- a/usersync/chooser_test.go +++ b/usersync/chooser_test.go @@ -1,12 +1,11 @@ package usersync import ( - "testing" - "time" - "github.com/prebid/prebid-server/privacy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "testing" + "time" ) func TestNewChooser(t *testing.T) { @@ -66,7 +65,7 @@ func TestChooserChoose(t *testing.T) { { description: "Cookie Opt Out", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a"}, @@ -80,7 +79,7 @@ func TestChooserChoose(t *testing.T) { { description: "GDPR Host Cookie Not Allowed", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a"}, @@ -94,7 +93,7 @@ func TestChooserChoose(t *testing.T) { { description: "No Bidders", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{}, @@ -108,7 +107,7 @@ func TestChooserChoose(t *testing.T) { { description: "One Bidder - Sync", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a"}, @@ -122,7 +121,7 @@ func TestChooserChoose(t *testing.T) { { description: "One Bidder - No Sync", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"c"}, @@ -136,7 +135,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - All Sync - Limit Disabled With 0", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a", "b"}, @@ -150,7 +149,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - All Sync - Limit Disabled With Negative Value", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: -1, }, givenChosenBidders: []string{"a", "b"}, @@ -164,7 +163,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - Limited Sync", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 1, }, givenChosenBidders: []string{"a", "b"}, @@ -178,7 +177,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - Limited Sync - Disqualified Syncers Don't Count Towards Limit", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 1, }, givenChosenBidders: []string{"c", "a", "b"}, @@ -192,7 +191,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - Some Sync, Some Don't", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a", "c"}, @@ -238,8 +237,8 @@ func TestChooserEvaluate(t *testing.T) { Redirect: NewUniformBidderFilter(BidderFilterModeExclude)} cookieNeedsSync := Cookie{} - cookieAlreadyHasSyncForA := Cookie{uids: map[string]uidWithExpiry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} - cookieAlreadyHasSyncForB := Cookie{uids: map[string]uidWithExpiry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} + cookieAlreadyHasSyncForA := Cookie{uids: map[string]UIDEntry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} + cookieAlreadyHasSyncForB := Cookie{uids: map[string]UIDEntry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} testCases := []struct { description string @@ -254,7 +253,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Valid", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: fakeSyncerA, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, @@ -263,7 +262,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Unknown Bidder", givenBidder: "unknown", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "unknown", Status: StatusUnknownBidder}, @@ -272,7 +271,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Duplicate Syncer", givenBidder: "a", givenSyncersSeen: map[string]struct{}{"keyA": {}}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusDuplicate}, @@ -281,7 +280,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Incompatible Kind", givenBidder: "b", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported}, @@ -290,7 +289,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Already Synced", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieAlreadyHasSyncForA, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusAlreadySynced}, @@ -299,7 +298,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Different Bidder Already Synced", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieAlreadyHasSyncForB, expectedSyncer: fakeSyncerA, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, @@ -308,7 +307,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Blocked By GDPR", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByGDPR}, @@ -317,11 +316,20 @@ func TestChooserEvaluate(t *testing.T) { description: "Blocked By CCPA", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByCCPA}, }, + { + description: "Blocked By activity control", + givenBidder: "a", + givenSyncersSeen: map[string]struct{}{}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: false}, + givenCookie: cookieNeedsSync, + expectedSyncer: nil, + expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy}, + }, } for _, test := range testCases { @@ -373,9 +381,10 @@ func (fakeSyncer) GetSync(syncTypes []SyncType, privacyPolicies privacy.Policies } type fakePrivacy struct { - gdprAllowsHostCookie bool - gdprAllowsBidderSync bool - ccpaAllowsBidderSync bool + gdprAllowsHostCookie bool + gdprAllowsBidderSync bool + ccpaAllowsBidderSync bool + activityAllowUserSync bool } func (p fakePrivacy) GDPRAllowsHostCookie() bool { @@ -389,3 +398,7 @@ func (p fakePrivacy) GDPRAllowsBidderSync(bidder string) bool { func (p fakePrivacy) CCPAAllowsBidderSync(bidder string) bool { return p.ccpaAllowsBidderSync } + +func (p fakePrivacy) ActivityAllowsUserSync(bidder string) bool { + return p.activityAllowUserSync +} diff --git a/usersync/cookie.go b/usersync/cookie.go index 5732e6c4c31..c0eb898c5ea 100644 --- a/usersync/cookie.go +++ b/usersync/cookie.go @@ -1,11 +1,10 @@ package usersync import ( - "encoding/base64" "encoding/json" "errors" - "math" "net/http" + "sort" "time" "github.com/prebid/prebid-server/config" @@ -20,72 +19,155 @@ const uidTTL = 14 * 24 * time.Hour // Cookie is the cookie used in Prebid Server. // -// To get an instance of this from a request, use ParseCookieFromRequest. -// To write an instance onto a response, use SetCookieOnResponse. +// To get an instance of this from a request, use ReadCookie. +// To write an instance onto a response, use WriteCookie. type Cookie struct { - uids map[string]uidWithExpiry - optOut bool - birthday *time.Time + uids map[string]UIDEntry + optOut bool } -// uidWithExpiry bundles the UID with an Expiration date. -// After the expiration, the UID is no longer valid. -type uidWithExpiry struct { +// UIDEntry bundles the UID with an Expiration date. +type UIDEntry struct { // UID is the ID given to a user by a particular bidder UID string `json:"uid"` // Expires is the time at which this UID should no longer apply. Expires time.Time `json:"expires"` } -// ParseCookieFromRequest parses the UserSyncMap from an HTTP Request. -func ParseCookieFromRequest(r *http.Request, cookie *config.HostCookie) *Cookie { - if cookie.OptOutCookie.Name != "" { - optOutCookie, err1 := r.Cookie(cookie.OptOutCookie.Name) - if err1 == nil && optOutCookie.Value == cookie.OptOutCookie.Value { - pc := NewCookie() - pc.SetOptOut(true) - return pc - } +// NewCookie returns a new empty cookie. +func NewCookie() *Cookie { + return &Cookie{ + uids: make(map[string]UIDEntry), } - var parsed *Cookie - uidCookie, err2 := r.Cookie(uidCookieName) - if err2 == nil { - parsed = ParseCookie(uidCookie) - } else { - parsed = NewCookie() +} + +// ReadCookie reads the cookie from the request +func ReadCookie(r *http.Request, decoder Decoder, host *config.HostCookie) *Cookie { + if hostOptOutCookie := checkHostCookieOptOut(r, host); hostOptOutCookie != nil { + return hostOptOutCookie } - // Fixes #582 - if uid, _, _ := parsed.GetUID(cookie.Family); uid == "" && cookie.CookieName != "" { - if hostCookie, err := r.Cookie(cookie.CookieName); err == nil { - parsed.TrySync(cookie.Family, hostCookie.Value) + + // Read cookie from request + cookieFromRequest, err := r.Cookie(uidCookieName) + if err != nil { + return NewCookie() + } + decodedCookie := decoder.Decode(cookieFromRequest.Value) + + return decodedCookie +} + +// PrepareCookieForWrite ejects UIDs as long as the cookie is too full +func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder) (string, error) { + uuidKeys := sortUIDs(cookie.uids) + + i := 0 + for len(cookie.uids) > 0 { + encodedCookie, err := encoder.Encode(cookie) + if err != nil { + return encodedCookie, nil + } + + // Convert to HTTP Cookie to Get Size + httpCookie := &http.Cookie{ + Name: uidCookieName, + Value: encodedCookie, + Expires: time.Now().Add(cfg.TTLDuration()), + Path: "/", } + cookieSize := len([]byte(httpCookie.String())) + + isCookieTooBig := cookieSize > cfg.MaxCookieSizeBytes && cfg.MaxCookieSizeBytes > 0 + if !isCookieTooBig { + return encodedCookie, nil + } + + uidToDelete := uuidKeys[i] + delete(cookie.uids, uidToDelete) + + i++ } - return parsed + return "", nil } -// ParseCookie parses the UserSync cookie from a raw HTTP cookie. -func ParseCookie(httpCookie *http.Cookie) *Cookie { - jsonValue, err := base64.URLEncoding.DecodeString(httpCookie.Value) - if err != nil { - // corrupted cookie; we should reset - return NewCookie() +// WriteCookie sets the prepared cookie onto the header +func WriteCookie(w http.ResponseWriter, encodedCookie string, cfg *config.HostCookie, setSiteCookie bool) { + ttl := cfg.TTLDuration() + + httpCookie := &http.Cookie{ + Name: uidCookieName, + Value: encodedCookie, + Expires: time.Now().Add(ttl), + Path: "/", } - var cookie Cookie - if err = json.Unmarshal(jsonValue, &cookie); err != nil { - // corrupted cookie; we should reset - return NewCookie() + if cfg.Domain != "" { + httpCookie.Domain = cfg.Domain } - return &cookie + if setSiteCookie { + httpCookie.Secure = true + httpCookie.SameSite = http.SameSiteNoneMode + } + + w.Header().Add("Set-Cookie", httpCookie.String()) } -// NewCookie returns a new empty cookie. -func NewCookie() *Cookie { - return &Cookie{ - uids: make(map[string]uidWithExpiry), - birthday: timestamp(), +// Sync tries to set the UID for some syncer key. It returns an error if the set didn't happen. +func (cookie *Cookie) Sync(key string, uid string) error { + if !cookie.AllowSyncs() { + return errors.New("the user has opted out of prebid server cookie syncs") } + + if checkAudienceNetwork(key, uid) { + return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\"") + } + + // Sync + cookie.uids[key] = UIDEntry{ + UID: uid, + Expires: time.Now().Add(uidTTL), + } + + return nil +} + +// sortUIDs is used to get a list of uids sorted from oldest to newest +// This list is used to eject oldest uids from the cookie +// This will be incorporated with a more complex ejection framework in a future PR +func sortUIDs(uids map[string]UIDEntry) []string { + if len(uids) > 0 { + uuidKeys := make([]string, 0, len(uids)) + for key := range uids { + uuidKeys = append(uuidKeys, key) + } + sort.SliceStable(uuidKeys, func(i, j int) bool { + return uids[uuidKeys[i]].Expires.Before(uids[uuidKeys[j]].Expires) + }) + return uuidKeys + } + return nil +} + +// SyncHostCookie syncs the request cookie with the host cookie +func SyncHostCookie(r *http.Request, requestCookie *Cookie, host *config.HostCookie) { + if uid, _, _ := requestCookie.GetUID(host.Family); uid == "" && host.CookieName != "" { + if hostCookie, err := r.Cookie(host.CookieName); err == nil { + requestCookie.Sync(host.Family, hostCookie.Value) + } + } +} + +func checkHostCookieOptOut(r *http.Request, host *config.HostCookie) *Cookie { + if host.OptOutCookie.Name != "" { + optOutCookie, err := r.Cookie(host.OptOutCookie.Name) + if err == nil && optOutCookie.Value == host.OptOutCookie.Value { + hostOptOut := NewCookie() + hostOptOut.SetOptOut(true) + return hostOptOut + } + } + return nil } // AllowSyncs is true if the user lets bidders sync cookies, and false otherwise. @@ -98,30 +180,12 @@ func (cookie *Cookie) SetOptOut(optOut bool) { cookie.optOut = optOut if optOut { - cookie.uids = make(map[string]uidWithExpiry) - } -} - -// Gets an HTTP cookie containing all the data from this UserSyncMap. This is a snapshot--not a live view. -func (cookie *Cookie) ToHTTPCookie(ttl time.Duration) *http.Cookie { - j, _ := json.Marshal(cookie) - b64 := base64.URLEncoding.EncodeToString(j) - - return &http.Cookie{ - Name: uidCookieName, - Value: b64, - Expires: time.Now().Add(ttl), - Path: "/", + cookie.uids = make(map[string]UIDEntry) } } // GetUID Gets this user's ID for the given syncer key. -// The first returned value is the user's ID. -// The second returned value is true if we had a value stored, and false if we didn't. -// The third returned value is true if that value is "active", and false if it's expired. -// -// If no value was stored, then the "isActive" return value will be false. -func (cookie *Cookie) GetUID(key string) (string, bool, bool) { +func (cookie *Cookie) GetUID(key string) (uid string, isUIDFound bool, isUIDActive bool) { if cookie != nil { if uid, ok := cookie.uids[key]; ok { return uid.UID, true, time.Now().Before(uid.Expires) @@ -142,41 +206,6 @@ func (cookie *Cookie) GetUIDs() map[string]string { return uids } -// SetCookieOnResponse is a shortcut for "ToHTTPCookie(); cookie.setDomain(domain); setCookie(w, cookie)" -func (cookie *Cookie) SetCookieOnResponse(w http.ResponseWriter, setSiteCookie bool, cfg *config.HostCookie, ttl time.Duration) { - httpCookie := cookie.ToHTTPCookie(ttl) - var domain string = cfg.Domain - - if domain != "" { - httpCookie.Domain = domain - } - - var currSize int = len([]byte(httpCookie.String())) - for cfg.MaxCookieSizeBytes > 0 && currSize > cfg.MaxCookieSizeBytes && len(cookie.uids) > 0 { - var oldestElem string = "" - var oldestDate int64 = math.MaxInt64 - for key, value := range cookie.uids { - timeUntilExpiration := time.Until(value.Expires) - if timeUntilExpiration < time.Duration(oldestDate) { - oldestElem = key - oldestDate = int64(timeUntilExpiration) - } - } - delete(cookie.uids, oldestElem) - httpCookie = cookie.ToHTTPCookie(ttl) - if domain != "" { - httpCookie.Domain = domain - } - currSize = len([]byte(httpCookie.String())) - } - - if setSiteCookie { - httpCookie.Secure = true - httpCookie.SameSite = http.SameSiteNoneMode - } - w.Header().Add("Set-Cookie", httpCookie.String()) -} - // Unsync removes the user's ID for the given syncer key from this cookie. func (cookie *Cookie) Unsync(key string) { delete(cookie.uids, key) @@ -201,24 +230,8 @@ func (cookie *Cookie) HasAnyLiveSyncs() bool { return false } -// TrySync tries to set the UID for some syncer key. It returns an error if the set didn't happen. -func (cookie *Cookie) TrySync(key string, uid string) error { - if !cookie.AllowSyncs() { - return errors.New("The user has opted out of prebid server cookie syncs.") - } - - // At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook. - // They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID. - if key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0" { - return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\".") - } - - cookie.uids[key] = uidWithExpiry{ - UID: uid, - Expires: time.Now().Add(uidTTL), - } - - return nil +func checkAudienceNetwork(key string, uid string) bool { + return key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0" } // cookieJson defines the JSON contract for the cookie data's storage format. @@ -226,70 +239,39 @@ func (cookie *Cookie) TrySync(key string, uid string) error { // This exists so that Cookie (which is public) can have private fields, and the rest of // the code doesn't have to worry about the cookie data storage format. type cookieJson struct { - LegacyUIDs map[string]string `json:"uids,omitempty"` - UIDs map[string]uidWithExpiry `json:"tempUIDs,omitempty"` - OptOut bool `json:"optout,omitempty"` - Birthday *time.Time `json:"bday,omitempty"` + UIDs map[string]UIDEntry `json:"tempUIDs,omitempty"` + OptOut bool `json:"optout,omitempty"` } func (cookie *Cookie) MarshalJSON() ([]byte, error) { return json.Marshal(cookieJson{ - UIDs: cookie.uids, - OptOut: cookie.optOut, - Birthday: cookie.birthday, + UIDs: cookie.uids, + OptOut: cookie.optOut, }) } -// UnmarshalJSON holds some transition code. -// -// "Legacy" cookies had UIDs *without* expiration dates, and recognized "0" as a legitimate UID for audienceNetwork. -// "Current" cookies always include UIDs with expiration dates, and never allow "0" for audienceNetwork. -// -// This Unmarshal method interprets both data formats, and does some conversions on legacy data to make it current. -// If you're seeing this message after March 2018, it's safe to assume that all the legacy cookies have been -// updated and remove the legacy logic. func (cookie *Cookie) UnmarshalJSON(b []byte) error { var cookieContract cookieJson - err := json.Unmarshal(b, &cookieContract) - if err == nil { - cookie.optOut = cookieContract.OptOut - cookie.birthday = cookieContract.Birthday - - if cookie.optOut { - cookie.uids = make(map[string]uidWithExpiry) - } else { - cookie.uids = cookieContract.UIDs - - if cookie.uids == nil { - cookie.uids = make(map[string]uidWithExpiry, len(cookieContract.LegacyUIDs)) - } + if err := json.Unmarshal(b, &cookieContract); err != nil { + return err + } - // Interpret "legacy" UIDs as having been expired already. - // This should cause us to re-sync, since it would be time for a new one. - for bidder, uid := range cookieContract.LegacyUIDs { - if _, ok := cookie.uids[bidder]; !ok { - cookie.uids[bidder] = uidWithExpiry{ - UID: uid, - Expires: time.Now().Add(-5 * time.Minute), - } - } - } + cookie.optOut = cookieContract.OptOut - // Any "0" values from audienceNetwork really meant "no ID available." This happens if they've never - // logged into Facebook. However... once we know a user's ID, we stop trying to re-sync them until the - // expiration date has passed. - // - // Since users may log into facebook later, this is a bad strategy. - // Since "0" is a fake ID for this bidder, we'll just treat it like it doesn't exist. - if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" { - delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork)) - } - } + if cookie.optOut { + cookie.uids = nil + } else { + cookie.uids = cookieContract.UIDs + } + + if cookie.uids == nil { + cookie.uids = make(map[string]UIDEntry) } - return err -} -func timestamp() *time.Time { - birthday := time.Now() - return &birthday + // Audience Network Handling + if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" { + delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork)) + } + + return nil } diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go index 4e87db5dd0a..5b86df54441 100644 --- a/usersync/cookie_test.go +++ b/usersync/cookie_test.go @@ -1,186 +1,597 @@ package usersync import ( - "encoding/base64" - "encoding/json" + "errors" "net/http" "net/http/httptest" - "strings" "testing" "time" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) -func TestOptOutCookie(t *testing.T) { - cookie := &Cookie{ - uids: make(map[string]uidWithExpiry), - optOut: true, - birthday: timestamp(), +func TestReadCookie(t *testing.T) { + testCases := []struct { + name string + givenRequest *http.Request + givenHttpCookie *http.Cookie + givenCookie *Cookie + givenDecoder Decoder + expectedCookie *Cookie + }{ + { + name: "simple-cookie", + givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil), + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + }, + }, + optOut: false, + }, + }, + { + name: "empty-cookie", + givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil), + givenCookie: &Cookie{}, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: false, + }, + }, + { + name: "nil-cookie", + givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil), + givenCookie: nil, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: false, + }, + }, + { + name: "corruptted-http-cookie", + givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil), + givenHttpCookie: &http.Cookie{ + Name: "uids", + Value: "bad base64 encoding", + }, + givenCookie: nil, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: false, + }, + }, } - ensureConsistency(t, cookie) -} -func TestEmptyOptOutCookie(t *testing.T) { - cookie := &Cookie{ - uids: make(map[string]uidWithExpiry), - optOut: true, - birthday: timestamp(), + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + if test.givenCookie != nil { + httpCookie, err := ToHTTPCookie(test.givenCookie) + assert.NoError(t, err) + test.givenRequest.AddCookie(httpCookie) + } else if test.givenCookie == nil && test.givenHttpCookie != nil { + test.givenRequest.AddCookie(test.givenHttpCookie) + } + actualCookie := ReadCookie(test.givenRequest, Base64Decoder{}, &config.HostCookie{}) + assert.Equal(t, test.expectedCookie.uids, actualCookie.uids) + assert.Equal(t, test.expectedCookie.optOut, actualCookie.optOut) + }) } - ensureConsistency(t, cookie) } -func TestEmptyCookie(t *testing.T) { - cookie := &Cookie{ - uids: make(map[string]uidWithExpiry), - optOut: false, - birthday: timestamp(), +func TestWriteCookie(t *testing.T) { + encoder := Base64Encoder{} + decoder := Base64Decoder{} + + testCases := []struct { + name string + givenCookie *Cookie + givenSetSiteCookie bool + expectedCookie *Cookie + }{ + { + name: "simple-cookie", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + givenSetSiteCookie: false, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + }, + { + name: "simple-cookie-opt-out", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: true, + }, + givenSetSiteCookie: true, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: true, + }, + }, + { + name: "cookie-multiple-uids", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + "rubicon": { + UID: "UID2", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + givenSetSiteCookie: true, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + "rubicon": { + UID: "UID2", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + }, } - ensureConsistency(t, cookie) -} -func TestCookieWithData(t *testing.T) { - cookie := newSampleCookie() - ensureConsistency(t, cookie) + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + // Write Cookie + w := httptest.NewRecorder() + encodedCookie, err := encoder.Encode(test.givenCookie) + assert.NoError(t, err) + WriteCookie(w, encodedCookie, &config.HostCookie{}, test.givenSetSiteCookie) + writtenCookie := w.Header().Get("Set-Cookie") + + // Read Cookie + header := http.Header{} + header.Add("Cookie", writtenCookie) + r := &http.Request{Header: header} + actualCookie := ReadCookie(r, decoder, &config.HostCookie{}) + + assert.Equal(t, test.expectedCookie, actualCookie) + }) + } } -func TestBidderNameGets(t *testing.T) { - cookie := newSampleCookie() - id, exists, _ := cookie.GetUID("adnxs") - if !exists { - t.Errorf("Cookie missing expected Appnexus ID") - } - if id != "123" { - t.Errorf("Bad appnexus id. Expected %s, got %s", "123", id) +func TestSync(t *testing.T) { + testCases := []struct { + name string + givenCookie *Cookie + givenSyncerKey string + givenUID string + expectedCookie *Cookie + expectedError error + }{ + { + name: "simple-sync", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{}, + }, + givenSyncerKey: "adnxs", + givenUID: "123", + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "123", + }, + }, + }, + }, + { + name: "dont-allow-syncs", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: true, + }, + givenSyncerKey: "adnxs", + givenUID: "123", + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + }, + expectedError: errors.New("the user has opted out of prebid server cookie syncs"), + }, + { + name: "audienceNetwork", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{}, + }, + givenSyncerKey: string(openrtb_ext.BidderAudienceNetwork), + givenUID: "0", + expectedError: errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\""), + }, } - id, exists, _ = cookie.GetUID("rubicon") - if !exists { - t.Errorf("Cookie missing expected Rubicon ID") - } - if id != "456" { - t.Errorf("Bad rubicon id. Expected %s, got %s", "456", id) + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + err := test.givenCookie.Sync(test.givenSyncerKey, test.givenUID) + if test.expectedError != nil { + assert.Equal(t, test.expectedError, err) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expectedCookie.uids[test.givenSyncerKey].UID, test.givenCookie.uids[test.givenSyncerKey].UID) + } + }) } } -func TestRejectAudienceNetworkCookie(t *testing.T) { - raw := &Cookie{ - uids: map[string]uidWithExpiry{ - "audienceNetwork": newTempId("0", 10), +func TestGetUIDs(t *testing.T) { + testCases := []struct { + name string + givenCookie *Cookie + expectedCookie *Cookie + expectedLen int + }{ + { + name: "two-uids", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "123", + }, + "rubicon": { + UID: "456", + }, + }, + }, + expectedLen: 2, + }, + { + name: "one-uid", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "123", + }, + }, + }, + expectedLen: 1, + }, + { + name: "empty", + givenCookie: &Cookie{}, + expectedLen: 0, + }, + { + name: "nil", + givenCookie: nil, + expectedLen: 0, }, - optOut: false, - birthday: timestamp(), - } - parsed := ParseCookie(raw.ToHTTPCookie(90 * 24 * time.Hour)) - if parsed.HasLiveSync("audienceNetwork") { - t.Errorf("Cookie serializing and deserializing should delete audienceNetwork values of 0") } - err := parsed.TrySync("audienceNetwork", "0") - if err == nil { - t.Errorf("Cookie should reject audienceNetwork values of 0.") - } - if parsed.HasLiveSync("audienceNetwork") { - t.Errorf("Cookie The cookie should have rejected the audienceNetwork sync.") + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + uids := test.givenCookie.GetUIDs() + assert.Len(t, uids, test.expectedLen) + for key, value := range uids { + assert.Equal(t, test.givenCookie.uids[key].UID, value) + } + + }) } } -func TestOptOutReset(t *testing.T) { - cookie := newSampleCookie() +func TestWriteCookieUserAgent(t *testing.T) { + encoder := Base64Encoder{} - cookie.SetOptOut(true) - if cookie.AllowSyncs() { - t.Error("After SetOptOut(true), a cookie should not allow more user syncs.") + testCases := []struct { + name string + givenUserAgent string + givenCookie *Cookie + givenHostCookie config.HostCookie + givenSetSiteCookie bool + expectedContains string + expectedNotContains string + }{ + { + name: "same-site-none", + givenUserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + givenHostCookie: config.HostCookie{}, + givenSetSiteCookie: true, + expectedContains: "; Secure;", + }, + { + name: "older-chrome-version", + givenUserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3770.142 Safari/537.36", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + givenHostCookie: config.HostCookie{}, + givenSetSiteCookie: true, + expectedNotContains: "SameSite=none", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + // Set Up + req := httptest.NewRequest("GET", "http://www.prebid.com", nil) + req.Header.Set("User-Agent", test.givenUserAgent) + + // Write Cookie + w := httptest.NewRecorder() + encodedCookie, err := encoder.Encode(test.givenCookie) + assert.NoError(t, err) + WriteCookie(w, encodedCookie, &test.givenHostCookie, test.givenSetSiteCookie) + writtenCookie := w.Header().Get("Set-Cookie") + + if test.expectedContains == "" { + assert.NotContains(t, writtenCookie, test.expectedNotContains) + } else { + assert.Contains(t, writtenCookie, test.expectedContains) + } + }) } - ensureConsistency(t, cookie) } -func TestOptIn(t *testing.T) { - cookie := &Cookie{ - uids: make(map[string]uidWithExpiry), - optOut: true, - birthday: timestamp(), +func TestPrepareCookieForWrite(t *testing.T) { + encoder := Base64Encoder{} + decoder := Base64Decoder{} + cookieToSend := &Cookie{ + uids: map[string]UIDEntry{ + "1": newTempId("1234567890123456789012345678901234567890123456", 7), + "7": newTempId("abcdefghijklmnopqrstuvwxy", 1), + "2": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6), + "3": newTempId("123456789012345678901234567896123456789012345678", 5), + "4": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4), + "5": newTempId("12345678901234567890123456789012345678901234567890", 3), + "6": newTempId("abcdefghij", 2), + }, + optOut: false, } - cookie.SetOptOut(false) - if !cookie.AllowSyncs() { - t.Error("After SetOptOut(false), a cookie should allow more user syncs.") + testCases := []struct { + name string + givenMaxCookieSize int + expectedRemainingUidKeys []string + }{ + { + name: "no-uids-ejected", + givenMaxCookieSize: 2000, + expectedRemainingUidKeys: []string{ + "1", "2", "3", "4", "5", "6", "7", + }, + }, + { + name: "no-uids-ejected-2", + givenMaxCookieSize: 0, + expectedRemainingUidKeys: []string{ + "1", "2", "3", "4", "5", "6", "7", + }, + }, + { + name: "one-uid-ejected", + givenMaxCookieSize: 900, + expectedRemainingUidKeys: []string{ + "1", "2", "3", "4", "5", "6", + }, + }, + { + name: "four-uids-ejected", + givenMaxCookieSize: 500, + expectedRemainingUidKeys: []string{ + "1", "2", "3", + }, + }, + { + name: "all-but-one-uids-ejected", + givenMaxCookieSize: 300, + expectedRemainingUidKeys: []string{ + "1", + }, + }, + { + name: "all-uids-ejected", + givenMaxCookieSize: 100, + expectedRemainingUidKeys: []string{}, + }, + { + name: "invalid-max-size", + givenMaxCookieSize: -100, + expectedRemainingUidKeys: []string{}, + }, } - ensureConsistency(t, cookie) -} -func TestParseCorruptedCookie(t *testing.T) { - raw := http.Cookie{ - Name: "uids", - Value: "bad base64 encoding", + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + encodedCookie, err := cookieToSend.PrepareCookieForWrite(&config.HostCookie{MaxCookieSizeBytes: test.givenMaxCookieSize}, encoder) + assert.NoError(t, err) + decodedCookie := decoder.Decode(encodedCookie) + + for _, key := range test.expectedRemainingUidKeys { + _, ok := decodedCookie.uids[key] + assert.Equal(t, true, ok) + } + assert.Equal(t, len(decodedCookie.uids), len(test.expectedRemainingUidKeys)) + }) } - parsed := ParseCookie(&raw) - ensureEmptyMap(t, parsed) } -func TestParseCorruptedCookieJSON(t *testing.T) { - cookieData := base64.URLEncoding.EncodeToString([]byte("bad json")) - raw := http.Cookie{ - Name: "uids", - Value: cookieData, +func TestSyncHostCookie(t *testing.T) { + testCases := []struct { + name string + givenCookie *Cookie + givenUID string + givenHostCookie *config.HostCookie + expectedCookie *Cookie + expectedError error + }{ + { + name: "simple-sync", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{}, + }, + givenHostCookie: &config.HostCookie{ + Family: "syncer", + CookieName: "adnxs", + }, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "syncer": { + UID: "some-user-id", + }, + }, + }, + }, + { + name: "uids-already-present", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "some-syncer": { + UID: "some-other-user-id", + }, + }, + }, + givenHostCookie: &config.HostCookie{ + Family: "syncer", + CookieName: "adnxs", + }, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "syncer": { + UID: "some-user-id", + }, + "some-syncer": { + UID: "some-other-user-id", + }, + }, + }, + }, + { + name: "host-already-synced", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "syncer": { + UID: "some-user-id", + }, + }, + }, + givenHostCookie: &config.HostCookie{ + Family: "syncer", + CookieName: "adnxs", + }, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "syncer": { + UID: "some-user-id", + }, + }, + }, + }, } - parsed := ParseCookie(&raw) - ensureEmptyMap(t, parsed) -} -func TestParseNilSyncMap(t *testing.T) { - cookieJSON := "{\"bday\":123,\"optout\":true}" - cookieData := base64.URLEncoding.EncodeToString([]byte(cookieJSON)) - raw := http.Cookie{ - Name: uidCookieName, - Value: cookieData, + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + r := httptest.NewRequest("POST", "http://www.prebid.com", nil) + r.AddCookie(&http.Cookie{ + Name: test.givenHostCookie.CookieName, + Value: "some-user-id", + }) + + SyncHostCookie(r, test.givenCookie, test.givenHostCookie) + for key, value := range test.expectedCookie.uids { + assert.Equal(t, value.UID, test.givenCookie.uids[key].UID) + } + }) } - parsed := ParseCookie(&raw) - ensureEmptyMap(t, parsed) - ensureConsistency(t, parsed) } -func TestParseOtherCookie(t *testing.T) { - req := httptest.NewRequest("POST", "http://www.prebid.com", nil) - otherCookieName := "other" - id := "some-user-id" - req.AddCookie(&http.Cookie{ - Name: otherCookieName, - Value: id, - }) - parsed := ParseCookieFromRequest(req, &config.HostCookie{ - Family: "adnxs", - CookieName: otherCookieName, - }) - val, _, _ := parsed.GetUID("adnxs") - if val != id { - t.Errorf("Bad cookie value. Expected %s, got %s", id, val) +func TestBidderNameGets(t *testing.T) { + cookie := newSampleCookie() + id, exists, _ := cookie.GetUID("adnxs") + if !exists { + t.Errorf("Cookie missing expected Appnexus ID") + } + if id != "123" { + t.Errorf("Bad appnexus id. Expected %s, got %s", "123", id) + } + + id, exists, _ = cookie.GetUID("rubicon") + if !exists { + t.Errorf("Cookie missing expected Rubicon ID") + } + if id != "456" { + t.Errorf("Bad rubicon id. Expected %s, got %s", "456", id) } } -func TestParseCookieFromRequestOptOut(t *testing.T) { +func TestReadCookieOptOut(t *testing.T) { optOutCookieName := "optOutCookieName" optOutCookieValue := "optOutCookieValue" + decoder := Base64Decoder{} - existingCookie := *(&Cookie{ - uids: map[string]uidWithExpiry{ + cookie := *(&Cookie{ + uids: map[string]UIDEntry{ "foo": newTempId("fooID", 1), "bar": newTempId("barID", 2), }, - optOut: false, - birthday: timestamp(), - }).ToHTTPCookie(24 * time.Hour) + optOut: false, + }) + + existingCookie, _ := ToHTTPCookie(&cookie) testCases := []struct { description string - givenExistingCookies []http.Cookie + givenExistingCookies []*http.Cookie expectedEmpty bool expectedSetOptOut bool }{ { description: "Opt Out Cookie", - givenExistingCookies: []http.Cookie{ + givenExistingCookies: []*http.Cookie{ existingCookie, {Name: optOutCookieName, Value: optOutCookieValue}}, expectedEmpty: true, @@ -188,14 +599,14 @@ func TestParseCookieFromRequestOptOut(t *testing.T) { }, { description: "No Opt Out Cookie", - givenExistingCookies: []http.Cookie{ + givenExistingCookies: []*http.Cookie{ existingCookie}, expectedEmpty: false, expectedSetOptOut: false, }, { description: "Opt Out Cookie - Wrong Value", - givenExistingCookies: []http.Cookie{ + givenExistingCookies: []*http.Cookie{ existingCookie, {Name: optOutCookieName, Value: "wrong"}}, expectedEmpty: false, @@ -203,7 +614,7 @@ func TestParseCookieFromRequestOptOut(t *testing.T) { }, { description: "Opt Out Cookie - Wrong Name", - givenExistingCookies: []http.Cookie{ + givenExistingCookies: []*http.Cookie{ existingCookie, {Name: "wrong", Value: optOutCookieValue}}, expectedEmpty: false, @@ -211,7 +622,7 @@ func TestParseCookieFromRequestOptOut(t *testing.T) { }, { description: "Opt Out Cookie - No Host Cookies", - givenExistingCookies: []http.Cookie{ + givenExistingCookies: []*http.Cookie{ {Name: optOutCookieName, Value: optOutCookieValue}}, expectedEmpty: true, expectedSetOptOut: true, @@ -222,10 +633,10 @@ func TestParseCookieFromRequestOptOut(t *testing.T) { req := httptest.NewRequest("POST", "http://www.prebid.com", nil) for _, c := range test.givenExistingCookies { - req.AddCookie(&c) + req.AddCookie(c) } - parsed := ParseCookieFromRequest(req, &config.HostCookie{ + parsed := ReadCookie(req, decoder, &config.HostCookie{ Family: "foo", OptOutCookie: config.Cookie{ Name: optOutCookieName, @@ -242,152 +653,59 @@ func TestParseCookieFromRequestOptOut(t *testing.T) { } } -func TestCookieReadWrite(t *testing.T) { - cookie := newSampleCookie() - - received := writeThenRead(cookie, 0) - uid, exists, isLive := received.GetUID("adnxs") - if !exists || !isLive || uid != "123" { - t.Errorf("Received cookie should have the adnxs ID=123. Got %s", uid) - } - - uid, exists, isLive = received.GetUID("rubicon") - if !exists || !isLive || uid != "456" { - t.Errorf("Received cookie should have the rubicon ID=456. Got %s", uid) +func TestOptIn(t *testing.T) { + cookie := &Cookie{ + uids: make(map[string]UIDEntry), + optOut: true, } - assert.True(t, received.HasAnyLiveSyncs(), "Has Live Syncs") - assert.Len(t, received.uids, 2, "Sync Count") -} - -func TestPopulatedLegacyCookieRead(t *testing.T) { - legacyJson := `{"uids":{"adnxs":"123","audienceNetwork":"456"},"bday":"2017-08-03T21:04:52.629198911Z"}` - var cookie Cookie - json.Unmarshal([]byte(legacyJson), &cookie) - - if cookie.HasAnyLiveSyncs() { - t.Error("Expected 0 user syncs. Found at least 1.") - } - if cookie.HasLiveSync("adnxs") { - t.Errorf("Received cookie should act like it has no ID for adnxs.") - } - if cookie.HasLiveSync("audienceNetwork") { - t.Errorf("Received cookie should act like it has no ID for audienceNetwork.") + cookie.SetOptOut(false) + if !cookie.AllowSyncs() { + t.Error("After SetOptOut(false), a cookie should allow more user syncs.") } + ensureConsistency(t, cookie) } -func TestEmptyLegacyCookieRead(t *testing.T) { - legacyJson := `{"bday":"2017-08-29T18:54:18.393925772Z"}` - var cookie Cookie - json.Unmarshal([]byte(legacyJson), &cookie) +func TestOptOutReset(t *testing.T) { + cookie := newSampleCookie() - if cookie.HasAnyLiveSyncs() { - t.Error("Expected 0 user syncs. Found at least 1.") + cookie.SetOptOut(true) + if cookie.AllowSyncs() { + t.Error("After SetOptOut(true), a cookie should not allow more user syncs.") } + ensureConsistency(t, cookie) } -func TestNilCookie(t *testing.T) { - var nilCookie *Cookie - - if nilCookie.HasLiveSync("anything") { - t.Error("nil cookies should respond with false when asked if they have a sync") - } - - if nilCookie.HasAnyLiveSyncs() { - t.Error("nil cookies shouldn't have any syncs.") - } - - if nilCookie.AllowSyncs() { - t.Error("nil cookies shouldn't allow syncs to take place.") - } - - uid, hadUID, isLive := nilCookie.GetUID("anything") - - if uid != "" { - t.Error("nil cookies should return empty strings for the UID.") - } - if hadUID { - t.Error("nil cookies shouldn't claim to have a UID mapping.") - } - if isLive { - t.Error("nil cookies shouldn't report live UID mappings.") +func TestOptOutCookie(t *testing.T) { + cookie := &Cookie{ + uids: make(map[string]UIDEntry), + optOut: true, } + ensureConsistency(t, cookie) } -func TestGetUIDs(t *testing.T) { - cookie := newSampleCookie() - uids := cookie.GetUIDs() - - assert.Len(t, uids, 2, "GetUIDs should return user IDs for all bidders") - assert.Equal(t, "123", uids["adnxs"], "GetUIDs should return the correct user ID for each bidder") - assert.Equal(t, "456", uids["rubicon"], "GetUIDs should return the correct user ID for each bidder") -} - -func TestGetUIDsWithEmptyCookie(t *testing.T) { - cookie := &Cookie{} - uids := cookie.GetUIDs() - - assert.Len(t, uids, 0, "GetUIDs shouldn't return any user syncs for an empty cookie") -} - -func TestGetUIDsWithNilCookie(t *testing.T) { - var cookie *Cookie - uids := cookie.GetUIDs() - - assert.Len(t, uids, 0, "GetUIDs shouldn't return any user syncs for a nil cookie") -} - -func TestTrimCookiesClosestExpirationDates(t *testing.T) { - cookieToSend := &Cookie{ - uids: map[string]uidWithExpiry{ - "k1": newTempId("12345678901234567890123456789012345678901234567890", 7), - "k2": newTempId("abcdefghijklmnopqrstuvwxyz", 1), - "k3": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6), - "k4": newTempId("12345678901234567890123456789612345678901234567890", 5), - "k5": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4), - "k6": newTempId("12345678901234567890123456789012345678901234567890", 3), - "k7": newTempId("abcdefghijklmnopqrstuvwxyz", 2), - }, - optOut: false, - birthday: timestamp(), - } - - type aTest struct { - maxCookieSize int - expKeys []string - } - testCases := []aTest{ - {maxCookieSize: 2000, expKeys: []string{"k1", "k2", "k3", "k4", "k5", "k6", "k7"}}, //1 don't trim, set - {maxCookieSize: 0, expKeys: []string{"k1", "k2", "k3", "k4", "k5", "k6", "k7"}}, //2 unlimited size: don't trim, set - {maxCookieSize: 800, expKeys: []string{"k1", "k5", "k4", "k3"}}, //3 trim to size and set - {maxCookieSize: 500, expKeys: []string{"k1", "k3"}}, //4 trim to size and set - {maxCookieSize: 200, expKeys: []string{}}, //5 insufficient size, trim to zero length and set - {maxCookieSize: -100, expKeys: []string{}}, //6 invalid size, trim to zero length and set - } - for i := range testCases { - processedCookie := writeThenRead(cookieToSend, testCases[i].maxCookieSize) - - actualKeys := make([]string, 0, 7) - for key := range processedCookie.uids { - actualKeys = append(actualKeys, key) - } - - assert.ElementsMatch(t, testCases[i].expKeys, actualKeys, "[Test %d]", i+1) +func newTempId(uid string, offset int) UIDEntry { + return UIDEntry{ + UID: uid, + Expires: time.Now().Add(time.Duration(offset) * time.Minute).UTC(), } } -func ensureEmptyMap(t *testing.T, cookie *Cookie) { - if !cookie.AllowSyncs() { - t.Error("Empty cookies should allow user syncs.") - } - if cookie.HasAnyLiveSyncs() { - t.Error("Empty cookies shouldn't have any user syncs. Found at least 1.") +func newSampleCookie() *Cookie { + return &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": newTempId("123", 10), + "rubicon": newTempId("456", 10), + }, + optOut: false, } } func ensureConsistency(t *testing.T, cookie *Cookie) { + decoder := Base64Decoder{} + if cookie.AllowSyncs() { - err := cookie.TrySync("pulsepoint", "1") + err := cookie.Sync("pulsepoint", "1") if err != nil { t.Errorf("Cookie sync should succeed if the user has opted in.") } @@ -416,18 +734,23 @@ func ensureConsistency(t *testing.T, cookie *Cookie) { t.Error("If the user opted out, the PBSCookie should have no user syncs.") } - err := cookie.TrySync("adnxs", "123") + err := cookie.Sync("adnxs", "123") if err == nil { t.Error("TrySync should fail if the user has opted out of PBSCookie syncs, but it succeeded.") } } - - copiedCookie := ParseCookie(cookie.ToHTTPCookie(90 * 24 * time.Hour)) + httpCookie, err := ToHTTPCookie(cookie) + assert.NoError(t, err) + copiedCookie := decoder.Decode(httpCookie.Value) if copiedCookie.AllowSyncs() != cookie.AllowSyncs() { t.Error("The PBSCookie interface shouldn't let modifications happen if the user has opted out") } - assert.Equal(t, len(cookie.uids), len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.") + if cookie.optOut { + assert.Equal(t, 0, len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.") + } else { + assert.Equal(t, len(cookie.uids), len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.") + } for family, uid := range copiedCookie.uids { if !cookie.HasLiveSync(family) { @@ -446,62 +769,17 @@ func ensureConsistency(t *testing.T, cookie *Cookie) { } } -func newTempId(uid string, offset int) uidWithExpiry { - return uidWithExpiry{ - UID: uid, - Expires: time.Now().Add(time.Duration(offset) * time.Minute).UTC(), +func ToHTTPCookie(cookie *Cookie) (*http.Cookie, error) { + encoder := Base64Encoder{} + encodedCookie, err := encoder.Encode(cookie) + if err != nil { + return nil, nil } -} -func newSampleCookie() *Cookie { - return &Cookie{ - uids: map[string]uidWithExpiry{ - "adnxs": newTempId("123", 10), - "rubicon": newTempId("456", 10), - }, - optOut: false, - birthday: timestamp(), - } -} - -func writeThenRead(cookie *Cookie, maxCookieSize int) *Cookie { - w := httptest.NewRecorder() - hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: maxCookieSize} - cookie.SetCookieOnResponse(w, false, hostCookie, 90*24*time.Hour) - writtenCookie := w.HeaderMap.Get("Set-Cookie") - - header := http.Header{} - header.Add("Cookie", writtenCookie) - request := http.Request{Header: header} - return ParseCookieFromRequest(&request, hostCookie) -} - -func TestSetCookieOnResponseForSameSiteNone(t *testing.T) { - cookie := newSampleCookie() - w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://www.prebid.com", nil) - ua := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" - req.Header.Set("User-Agent", ua) - hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: 0} - cookie.SetCookieOnResponse(w, true, hostCookie, 90*24*time.Hour) - writtenCookie := w.HeaderMap.Get("Set-Cookie") - t.Log("Set-Cookie is: ", writtenCookie) - if !strings.Contains(writtenCookie, "; Secure;") { - t.Error("Set-Cookie should contain Secure") - } -} - -func TestSetCookieOnResponseForOlderChromeVersion(t *testing.T) { - cookie := newSampleCookie() - w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://www.prebid.com", nil) - ua := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3770.142 Safari/537.36" - req.Header.Set("User-Agent", ua) - hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: 0} - cookie.SetCookieOnResponse(w, false, hostCookie, 90*24*time.Hour) - writtenCookie := w.HeaderMap.Get("Set-Cookie") - t.Log("Set-Cookie is: ", writtenCookie) - if strings.Contains(writtenCookie, "SameSite=none") { - t.Error("Set-Cookie should not contain SameSite=none") - } + return &http.Cookie{ + Name: uidCookieName, + Value: encodedCookie, + Expires: time.Now().Add((90 * 24 * time.Hour)), + Path: "/", + }, nil } diff --git a/usersync/decoder.go b/usersync/decoder.go new file mode 100644 index 00000000000..3ff13aa3242 --- /dev/null +++ b/usersync/decoder.go @@ -0,0 +1,27 @@ +package usersync + +import ( + "encoding/base64" + "encoding/json" +) + +type Decoder interface { + // Decode takes an encoded string and decodes it into a cookie + Decode(v string) *Cookie +} + +type Base64Decoder struct{} + +func (d Base64Decoder) Decode(encodedValue string) *Cookie { + jsonValue, err := base64.URLEncoding.DecodeString(encodedValue) + if err != nil { + return NewCookie() + } + + var cookie Cookie + if err = json.Unmarshal(jsonValue, &cookie); err != nil { + return NewCookie() + } + + return &cookie +} diff --git a/usersync/encoder.go b/usersync/encoder.go new file mode 100644 index 00000000000..eef7e2ef34f --- /dev/null +++ b/usersync/encoder.go @@ -0,0 +1,23 @@ +package usersync + +import ( + "encoding/base64" + "encoding/json" +) + +type Encoder interface { + // Encode a cookie into a base 64 string + Encode(c *Cookie) (string, error) +} + +type Base64Encoder struct{} + +func (e Base64Encoder) Encode(c *Cookie) (string, error) { + j, err := json.Marshal(c) + if err != nil { + return "", err + } + b64 := base64.URLEncoding.EncodeToString(j) + + return b64, nil +} diff --git a/usersync/encoder_decoder_test.go b/usersync/encoder_decoder_test.go new file mode 100644 index 00000000000..5a87d4e7c82 --- /dev/null +++ b/usersync/encoder_decoder_test.go @@ -0,0 +1,149 @@ +package usersync + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEncoderDecoder(t *testing.T) { + encoder := Base64Encoder{} + decoder := Base64Decoder{} + + testCases := []struct { + name string + givenRequest *http.Request + givenHttpCookie *http.Cookie + givenCookie *Cookie + givenDecoder Decoder + expectedCookie *Cookie + }{ + { + name: "simple-cookie", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + }, + }, + optOut: false, + }, + }, + { + name: "empty-cookie", + givenCookie: &Cookie{}, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: false, + }, + }, + { + name: "nil-cookie", + givenCookie: nil, + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{}, + optOut: false, + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + encodedCookie, err := encoder.Encode(test.givenCookie) + assert.NoError(t, err) + decodedCookie := decoder.Decode(encodedCookie) + + assert.Equal(t, test.expectedCookie.uids, decodedCookie.uids) + assert.Equal(t, test.expectedCookie.optOut, decodedCookie.optOut) + }) + } +} + +func TestEncoder(t *testing.T) { + encoder := Base64Encoder{} + + testCases := []struct { + name string + givenCookie *Cookie + expectedEncodedCookie string + }{ + { + name: "simple-cookie", + givenCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + Expires: time.Time{}, + }, + }, + optOut: false, + }, + expectedEncodedCookie: "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJVSUQiLCJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19", + }, + { + name: "empty-cookie", + givenCookie: &Cookie{}, + expectedEncodedCookie: "e30=", + }, + { + name: "nil-cookie", + givenCookie: nil, + expectedEncodedCookie: "bnVsbA==", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + encodedCookie, err := encoder.Encode(test.givenCookie) + assert.NoError(t, err) + + assert.Equal(t, test.expectedEncodedCookie, encodedCookie) + }) + } +} + +func TestDecoder(t *testing.T) { + decoder := Base64Decoder{} + + testCases := []struct { + name string + givenEncodedCookie string + expectedCookie *Cookie + }{ + { + name: "simple-encoded-cookie", + givenEncodedCookie: "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJVSUQiLCJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19", + expectedCookie: &Cookie{ + uids: map[string]UIDEntry{ + "adnxs": { + UID: "UID", + }, + }, + optOut: false, + }, + }, + { + name: "nil-encoded-cookie", + givenEncodedCookie: "", + expectedCookie: NewCookie(), + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + decodedCookie := decoder.Decode(test.givenEncodedCookie) + assert.Equal(t, test.expectedCookie, decodedCookie) + }) + } +} diff --git a/usersync/syncer.go b/usersync/syncer.go index 2e0e41027b5..fb14201f2dc 100644 --- a/usersync/syncer.go +++ b/usersync/syncer.go @@ -63,7 +63,7 @@ var ErrSyncerKeyRequired = errors.New("key is required") // NewSyncer creates a new Syncer from the provided configuration, or return an error if macro substition // fails or an endpoint url is invalid. -func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer) (Syncer, error) { +func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder string) (Syncer, error) { if syncerConfig.Key == "" { return nil, ErrSyncerKeyRequired } @@ -80,7 +80,7 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer) (Syncer, if syncerConfig.IFrame != nil { var err error - syncer.iframe, err = buildTemplate(syncerConfig.Key, setuidSyncTypeIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame) + syncer.iframe, err = buildTemplate(bidder, setuidSyncTypeIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame) if err != nil { return nil, fmt.Errorf("iframe %v", err) } @@ -91,7 +91,7 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer) (Syncer, if syncerConfig.Redirect != nil { var err error - syncer.redirect, err = buildTemplate(syncerConfig.Key, setuidSyncTypeRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect) + syncer.redirect, err = buildTemplate(bidder, setuidSyncTypeRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect) if err != nil { return nil, fmt.Errorf("redirect %v", err) } @@ -114,13 +114,14 @@ func resolveDefaultSyncType(syncerConfig config.Syncer) SyncType { var ( macroRegexExternalHost = regexp.MustCompile(`{{\s*\.ExternalURL\s*}}`) macroRegexSyncerKey = regexp.MustCompile(`{{\s*\.SyncerKey\s*}}`) + macroRegexBidderName = regexp.MustCompile(`{{\s*\.BidderName\s*}}`) macroRegexSyncType = regexp.MustCompile(`{{\s*\.SyncType\s*}}`) macroRegexUserMacro = regexp.MustCompile(`{{\s*\.UserMacro\s*}}`) macroRegexRedirect = regexp.MustCompile(`{{\s*\.RedirectURL\s*}}`) macroRegex = regexp.MustCompile(`{{\s*\..*?\s*}}`) ) -func buildTemplate(key, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint) (*template.Template, error) { +func buildTemplate(bidderName, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint) (*template.Template, error) { redirectTemplate := syncerEndpoint.RedirectURL if redirectTemplate == "" { redirectTemplate = hostConfig.RedirectURL @@ -128,7 +129,8 @@ func buildTemplate(key, syncTypeValue string, hostConfig config.UserSync, syncer externalURL := chooseExternalURL(syncerEndpoint.ExternalURL, syncerExternalURL, hostConfig.ExternalURL) - redirectURL := macroRegexSyncerKey.ReplaceAllLiteralString(redirectTemplate, key) + redirectURL := macroRegexSyncerKey.ReplaceAllLiteralString(redirectTemplate, bidderName) + redirectURL = macroRegexBidderName.ReplaceAllLiteralString(redirectURL, bidderName) redirectURL = macroRegexSyncType.ReplaceAllLiteralString(redirectURL, syncTypeValue) redirectURL = macroRegexUserMacro.ReplaceAllLiteralString(redirectURL, syncerEndpoint.UserMacro) redirectURL = macroRegexExternalHost.ReplaceAllLiteralString(redirectURL, externalURL) @@ -136,7 +138,7 @@ func buildTemplate(key, syncTypeValue string, hostConfig config.UserSync, syncer url := macroRegexRedirect.ReplaceAllString(syncerEndpoint.URL, redirectURL) - templateName := strings.ToLower(key) + "_usersync_url" + templateName := strings.ToLower(bidderName) + "_usersync_url" return template.New(templateName).Parse(url) } diff --git a/usersync/syncer_test.go b/usersync/syncer_test.go index 6e727c3a49c..c309b899358 100644 --- a/usersync/syncer_test.go +++ b/usersync/syncer_test.go @@ -26,6 +26,7 @@ func TestNewSyncer(t *testing.T) { testCases := []struct { description string givenKey string + givenBidderName string givenIFrameConfig *config.SyncerEndpoint givenRedirectConfig *config.SyncerEndpoint givenExternalURL string @@ -37,6 +38,7 @@ func TestNewSyncer(t *testing.T) { { description: "Missing Key", givenKey: "", + givenBidderName: "", givenIFrameConfig: iframeConfig, givenRedirectConfig: nil, expectedError: "key is required", @@ -44,6 +46,7 @@ func TestNewSyncer(t *testing.T) { { description: "Missing Endpoints", givenKey: "a", + givenBidderName: "bidderA", givenIFrameConfig: nil, givenRedirectConfig: nil, expectedError: "at least one endpoint (iframe and/or redirect) is required", @@ -51,6 +54,7 @@ func TestNewSyncer(t *testing.T) { { description: "IFrame & Redirect Endpoints", givenKey: "a", + givenBidderName: "bidderA", givenIFrameConfig: iframeConfig, givenRedirectConfig: redirectConfig, expectedDefault: SyncTypeIFrame, @@ -60,13 +64,15 @@ func TestNewSyncer(t *testing.T) { { description: "IFrame - Parse Error", givenKey: "a", + givenBidderName: "bidderA", givenIFrameConfig: errParseConfig, givenRedirectConfig: nil, - expectedError: "iframe template: a_usersync_url:1: function \"malformed\" not defined", + expectedError: "iframe template: biddera_usersync_url:1: function \"malformed\" not defined", }, { description: "IFrame - Validation Error", givenKey: "a", + givenBidderName: "bidderA", givenIFrameConfig: errInvalidConfig, givenRedirectConfig: nil, expectedError: "iframe composed url: \"notAURL:http%3A%2F%2Fhost.com%2Fhost\" is invalid", @@ -74,13 +80,15 @@ func TestNewSyncer(t *testing.T) { { description: "Redirect - Parse Error", givenKey: "a", + givenBidderName: "bidderA", givenIFrameConfig: nil, givenRedirectConfig: errParseConfig, - expectedError: "redirect template: a_usersync_url:1: function \"malformed\" not defined", + expectedError: "redirect template: biddera_usersync_url:1: function \"malformed\" not defined", }, { description: "Redirect - Validation Error", givenKey: "a", + givenBidderName: "bidderA", givenIFrameConfig: nil, givenRedirectConfig: errInvalidConfig, expectedError: "redirect composed url: \"notAURL:http%3A%2F%2Fhost.com%2Fhost\" is invalid", @@ -88,6 +96,7 @@ func TestNewSyncer(t *testing.T) { { description: "Syncer Level External URL", givenKey: "a", + givenBidderName: "bidderA", givenExternalURL: "http://syncer.com", givenIFrameConfig: iframeConfig, givenRedirectConfig: redirectConfig, @@ -106,7 +115,7 @@ func TestNewSyncer(t *testing.T) { ExternalURL: test.givenExternalURL, } - result, err := NewSyncer(hostConfig, syncerConfig) + result, err := NewSyncer(hostConfig, syncerConfig, test.givenBidderName) if test.expectedError == "" { assert.NoError(t, err, test.description+":err") @@ -196,7 +205,7 @@ func TestBuildTemplate(t *testing.T) { expectedRendered: "hasNoComposedMacros,gdpr=A", }, { - description: "All Composed Macros", + description: "All Composed Macros - SyncerKey", givenSyncerEndpoint: config.SyncerEndpoint{ URL: "https://bidder.com/sync?redirect={{.RedirectURL}}", RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&f={{.SyncType}}&gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&uid={{.UserMacro}}", @@ -205,6 +214,16 @@ func TestBuildTemplate(t *testing.T) { }, expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26gpp%3DD%26gpp_sid%3D1%26uid%3D%24UID%24", }, + { + description: "All Composed Macros - BidderName", + givenSyncerEndpoint: config.SyncerEndpoint{ + URL: "https://bidder.com/sync?redirect={{.RedirectURL}}", + RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.BidderName}}&f={{.SyncType}}&gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&uid={{.UserMacro}}", + ExternalURL: "http://syncer.com", + UserMacro: "$UID$", + }, + expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26gpp%3DD%26gpp_sid%3D1%26uid%3D%24UID%24", + }, { description: "Redirect URL + External URL From Host", givenSyncerEndpoint: config.SyncerEndpoint{ diff --git a/usersync/syncersbuilder.go b/usersync/syncersbuilder.go index c521d36dde2..9a52a740f31 100644 --- a/usersync/syncersbuilder.go +++ b/usersync/syncersbuilder.go @@ -59,17 +59,16 @@ func BuildSyncers(hostConfig *config.Configuration, bidderInfos config.BidderInf continue } - syncer, err := NewSyncer(hostUserSyncConfig, primaryCfg.cfg) - if err != nil { - errs = append(errs, SyncerBuildError{ - Bidder: primaryCfg.name, - SyncerKey: key, - Err: err, - }) - continue - } - for _, bidder := range cfgGroup { + syncer, err := NewSyncer(hostUserSyncConfig, primaryCfg.cfg, bidder.name) + if err != nil { + errs = append(errs, SyncerBuildError{ + Bidder: primaryCfg.name, + SyncerKey: key, + Err: err, + }) + continue + } syncers[bidder.name] = syncer } } diff --git a/usersync/syncersbuilder_test.go b/usersync/syncersbuilder_test.go index b3672a501bb..fbc1863959b 100644 --- a/usersync/syncersbuilder_test.go +++ b/usersync/syncersbuilder_test.go @@ -48,7 +48,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAPopulated}, expectedIFramesURLs: map[string]string{ - "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fa%2Fhost", + "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder1%2Fhost", }, }, { @@ -64,7 +64,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAError}, expectedErrors: []string{ - "cannot create syncer for bidder bidder1 with key a: iframe template: a_usersync_url:1: function \"xRedirectURL\" not defined", + "cannot create syncer for bidder bidder1 with key a: iframe template: bidder1_usersync_url:1: function \"xRedirectURL\" not defined", }, }, { @@ -72,8 +72,8 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAPopulated, "bidder2": infoKeyBPopulated}, expectedIFramesURLs: map[string]string{ - "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fa%2Fhost", - "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fb%2Fhost", + "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder1%2Fhost", + "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder2%2Fhost", }, }, { @@ -81,8 +81,8 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAPopulated, "bidder2": infoKeyAEmpty}, expectedIFramesURLs: map[string]string{ - "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fa%2Fhost", - "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fa%2Fhost", + "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder1%2Fhost", + "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder2%2Fhost", }, }, { @@ -106,7 +106,8 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAEmpty, "bidder2": infoKeyAError}, expectedErrors: []string{ - "cannot create syncer for bidder bidder2 with key a: iframe template: a_usersync_url:1: function \"xRedirectURL\" not defined", + "cannot create syncer for bidder bidder2 with key a: iframe template: bidder1_usersync_url:1: function \"xRedirectURL\" not defined", + "cannot create syncer for bidder bidder2 with key a: iframe template: bidder2_usersync_url:1: function \"xRedirectURL\" not defined", }, }, { @@ -114,7 +115,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": {}, "bidder2": infoKeyBPopulated}, expectedIFramesURLs: map[string]string{ - "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fb%2Fhost", + "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder2%2Fhost", }, }, { @@ -122,7 +123,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyADisabled, "bidder2": infoKeyBPopulated}, expectedIFramesURLs: map[string]string{ - "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fb%2Fhost", + "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder2%2Fhost", }, }, { @@ -130,7 +131,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyASupportsOnly, "bidder2": infoKeyBPopulated}, expectedIFramesURLs: map[string]string{ - "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fb%2Fhost", + "bidder2": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhost.com%2Fbidder2%2Fhost", }, }, { @@ -138,7 +139,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: hostConfig, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAError, "bidder2": infoKeyBEmpty}, expectedErrors: []string{ - "cannot create syncer for bidder bidder1 with key a: iframe template: a_usersync_url:1: function \"xRedirectURL\" not defined", + "cannot create syncer for bidder bidder1 with key a: iframe template: bidder1_usersync_url:1: function \"xRedirectURL\" not defined", "cannot create syncer for bidder bidder2 with key b: at least one endpoint (iframe and/or redirect) is required", }, }, @@ -147,7 +148,7 @@ func TestBuildSyncers(t *testing.T) { givenConfig: config.Configuration{ExternalURL: "http://host.com", UserSync: config.UserSync{ExternalURL: "http://hostoverride.com", RedirectURL: "{{.ExternalURL}}/{{.SyncerKey}}/host"}}, givenBidderInfos: map[string]config.BidderInfo{"bidder1": infoKeyAPopulated}, expectedIFramesURLs: map[string]string{ - "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhostoverride.com%2Fa%2Fhost", + "bidder1": "https://bidder.com/iframe?redirect=http%3A%2F%2Fhostoverride.com%2Fbidder1%2Fhost", }, }, } diff --git a/util/httputil/httputil.go b/util/httputil/httputil.go index 992364899ff..28334a54b87 100644 --- a/util/httputil/httputil.go +++ b/util/httputil/httputil.go @@ -14,6 +14,16 @@ var ( xRealIP = http.CanonicalHeaderKey("X-Real-IP") ) +type ContentEncoding string + +const ( + ContentEncodingGZIP ContentEncoding = "gzip" +) + +func (k ContentEncoding) Normalize() ContentEncoding { + return ContentEncoding(strings.ToLower(string(k))) +} + // FindIP returns the first ip address found in the http request matching the predicate v. func FindIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { if ip, ver := findTrueClientIP(r, v); ip != nil { diff --git a/util/iputil/parse.go b/util/iputil/parse.go index bcb00760e22..c226ece8e5e 100644 --- a/util/iputil/parse.go +++ b/util/iputil/parse.go @@ -15,6 +15,14 @@ const ( IPv6 IPVersion = 6 ) +const ( + IPv4BitSize = 32 + IPv6BitSize = 128 + + IPv4DefaultMaskingBitSize = 24 + IPv6DefaultMaskingBitSize = 56 +) + // ParseIP parses v as an ip address returning the result and version, or nil and unknown if invalid. func ParseIP(v string) (net.IP, IPVersion) { if ip := net.ParseIP(v); ip != nil { diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go index 49df0772ba6..c33740d456c 100644 --- a/util/maputil/maputil.go +++ b/util/maputil/maputil.go @@ -48,3 +48,16 @@ func HasElement(m map[string]interface{}, k ...string) bool { return exists } + +// Clone creates an indepent copy of a map, +func Clone[K comparable, V any](m map[K]V) map[K]V { + if m == nil { + return nil + } + clone := make(map[K]V, len(m)) + for key, value := range m { + clone[key] = value + } + + return clone +} diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go index 114c23327bd..e7c067224c7 100644 --- a/util/maputil/maputil_test.go +++ b/util/maputil/maputil_test.go @@ -245,3 +245,29 @@ func TestHasElement(t *testing.T) { assert.Equal(t, test.expected, result, test.description) } } + +func TestCloneMap(t *testing.T) { + // Test we handle nils properly + t.Run("NilMap", func(t *testing.T) { + var testMap, copyMap map[string]string = nil, nil // copyMap is a manual copy of testMap + clone := Clone(testMap) + testMap = map[string]string{"foo": "bar"} + assert.Equal(t, copyMap, clone) + }) + // Test a simple string map + t.Run("StringMap", func(t *testing.T) { + var testMap, copyMap map[string]string = map[string]string{"foo": "bar", "first": "one"}, map[string]string{"foo": "bar", "first": "one"} + clone := Clone(testMap) + testMap["foo"] = "baz" + testMap["bozo"] = "the clown" + assert.Equal(t, copyMap, clone) + }) + // Test a simple map[string]int + t.Run("StringInt", func(t *testing.T) { + var testMap, copyMap map[string]int = map[string]int{"foo": 1, "first": 2}, map[string]int{"foo": 1, "first": 2} + clone := Clone(testMap) + testMap["foo"] = 7 + testMap["bozo"] = 13 + assert.Equal(t, copyMap, clone) + }) +} diff --git a/util/ptrutil/ptrutil.go b/util/ptrutil/ptrutil.go index 999ac34a303..90888f3eef8 100644 --- a/util/ptrutil/ptrutil.go +++ b/util/ptrutil/ptrutil.go @@ -3,3 +3,12 @@ package ptrutil func ToPtr[T any](v T) *T { return &v } + +func Clone[T any](v *T) *T { + if v == nil { + return nil + } + + clone := *v + return &clone +} diff --git a/util/sliceutil/clone.go b/util/sliceutil/clone.go new file mode 100644 index 00000000000..2077a9336b2 --- /dev/null +++ b/util/sliceutil/clone.go @@ -0,0 +1,12 @@ +package sliceutil + +func Clone[T any](s []T) []T { + if s == nil { + return nil + } + + c := make([]T, len(s)) + copy(c, s) + + return c +} diff --git a/util/sliceutil/clone_test.go b/util/sliceutil/clone_test.go new file mode 100644 index 00000000000..6e905b5cbe1 --- /dev/null +++ b/util/sliceutil/clone_test.go @@ -0,0 +1,39 @@ +package sliceutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCloneSlice(t *testing.T) { + testCases := []struct { + name string + given []int + }{ + { + name: "nil", + given: nil, + }, + { + name: "empty", + given: []int{}, + }, + { + name: "one", + given: []int{1}, + }, + { + name: "many", + given: []int{1, 2}, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + result := Clone(test.given) + assert.Equal(t, test.given, result, "equality") + assert.NotSame(t, test.given, result, "pointer") + }) + } +} diff --git a/util/sliceutil/sliceutil.go b/util/sliceutil/containstring.go similarity index 100% rename from util/sliceutil/sliceutil.go rename to util/sliceutil/containstring.go diff --git a/util/sliceutil/sliceutil_test.go b/util/sliceutil/containstring_test.go similarity index 100% rename from util/sliceutil/sliceutil_test.go rename to util/sliceutil/containstring_test.go diff --git a/util/stringutil/stringutil.go b/util/stringutil/stringutil.go new file mode 100644 index 00000000000..0443a442978 --- /dev/null +++ b/util/stringutil/stringutil.go @@ -0,0 +1,25 @@ +package stringutil + +import ( + "strconv" + "strings" +) + +// StrToInt8Slice breaks a string into a series of tokens using a comma as a delimiter but only +// appends the tokens into the return array if tokens can be interpreted as an 'int8' +func StrToInt8Slice(str string) ([]int8, error) { + var r []int8 + + if len(str) > 0 { + strSlice := strings.Split(str, ",") + for _, s := range strSlice { + v, err := strconv.ParseInt(s, 10, 8) + if err != nil { + return nil, err + } + r = append(r, int8(v)) + } + } + + return r, nil +} diff --git a/util/stringutil/stringutil_test.go b/util/stringutil/stringutil_test.go new file mode 100644 index 00000000000..94988ee41c9 --- /dev/null +++ b/util/stringutil/stringutil_test.go @@ -0,0 +1,77 @@ +package stringutil + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStrToInt8Slice(t *testing.T) { + type testOutput struct { + arr []int8 + err error + } + tests := []struct { + desc string + in string + expected testOutput + }{ + { + desc: "empty string, expect nil output array", + in: "", + expected: testOutput{ + arr: nil, + err: nil, + }, + }, + { + desc: "string doesn't contain digits, expect nil output array", + in: "malformed", + expected: testOutput{ + arr: nil, + err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax}, + }, + }, + { + desc: "string contains int8 digits and non-digits, expect array with a single int8 element", + in: "malformed,2,malformed", + expected: testOutput{ + arr: nil, + err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax}, + }, + }, + { + desc: "string comes with single digit too big to fit into a signed int8, expect nil output array", + in: "128", + expected: testOutput{ + arr: nil, + err: &strconv.NumError{"ParseInt", "128", strconv.ErrRange}, + }, + }, + { + desc: "string comes with single digit that fits into 8 bits, expect array with a single int8 element", + in: "127", + expected: testOutput{ + arr: []int8{int8(127)}, + err: nil, + }, + }, + { + desc: "string comes with multiple, comma-separated numbers that fit into 8 bits, expect array with int8 elements", + in: "127,2,-127", + expected: testOutput{ + arr: []int8{int8(127), int8(2), int8(-127)}, + err: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + outArr, outErr := StrToInt8Slice(tt.in) + assert.Equal(t, tt.expected.arr, outArr, tt.desc) + assert.Equal(t, tt.expected.err, outErr, tt.desc) + }) + } +} diff --git a/validate.sh b/validate.sh index b81ade344d2..d263df0ab51 100755 --- a/validate.sh +++ b/validate.sh @@ -17,29 +17,7 @@ while true; do esac done -die() { echo -e "$@" 1>&2 ; exit 1; } - -# Build a list of all the top-level directories in the project. -for DIRECTORY in */ ; do - GOGLOB="$GOGLOB ${DIRECTORY%/}" -done -GOGLOB="${GOGLOB/ docs/}" -GOGLOB="${GOGLOB/ vendor/}" - -# Check that there are no formatting issues -GOFMT_LINES=`gofmt -s -l $GOGLOB | tr '\\\\' '/' | wc -l | xargs` -if $AUTOFMT; then - # if there are files with formatting issues, they will be automatically corrected using the gofmt -w command - if [[ $GOFMT_LINES -ne 0 ]]; then - FMT_FILES=`gofmt -s -l $GOGLOB | tr '\\\\' '/' | xargs` - for FILE in $FMT_FILES; do - echo "Running: gofmt -s -w $FILE" - `gofmt -s -w $FILE` - done - fi -else - test $GOFMT_LINES -eq 0 || die "gofmt needs to be run, ${GOFMT_LINES} files have issues. Below is a list of files to review:\n`gofmt -s -l $GOGLOB`" -fi +./scripts/format.sh -f $AUTOFMT # Run the actual tests. Make sure there's enough coverage too, if the flags call for it. if $COVERAGE; then @@ -57,7 +35,6 @@ if [ "$RACE" -ne "0" ]; then fi if $VET; then - COMMAND="go vet" - echo "Running: $COMMAND" - `$COMMAND` + echo "Running go vet check" + go vet -composites=false ./... fi