diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f15985dc6a4..a5f5cdf5aaf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,31 +1,34 @@ ### ๐Ÿ”ง Type of changes - [ ] new bid adapter -- [ ] update bid adapter +- [ ] bid adapter update - [ ] new feature - [ ] new analytics adapter - [ ] new module +- [ ] module update - [ ] bugfix - [ ] documentation - [ ] configuration +- [ ] dependency update - [ ] tech debt (test coverage, refactorings, etc.) ### โœจ What's the context? - -What's the context for the changes? Are there any - +What's the context for the changes? ### ๐Ÿง  Rationale behind the change - Why did you choose to make these changes? Were there any trade-offs you had to consider? +### ๐Ÿ”Ž New Bid Adapter Checklist +- [ ] verify email contact works +- [ ] NO fully dynamic hostnames +- [ ] geographic host parameters are NOT required +- [ ] direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job* +- [ ] if the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter* +- [ ] cover an adapter configuration with an integration test ### ๐Ÿงช Test plan - How do you know the changes are safe to ship to production? - ### ๐ŸŽ Quality check - - [ ] Are your changes following [our code style guidelines](https://github.com/prebid/prebid-server-java/blob/master/docs/developers/code-style.md)? - [ ] Are there any breaking changes in your code? - [ ] Does your test coverage exceed 90%? diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000000..a86f0b144a5 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,48 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 21 + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Build with Maven + run: mvn -B package --file extra/pom.xml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml index 4249a8370a9..39964eb69aa 100644 --- a/.github/workflows/docker-image-publish.yml +++ b/.github/workflows/docker-image-publish.yml @@ -1,10 +1,9 @@ name: Publish Docker image for new tag/release on: - workflow_run: - workflows: [Publish release] - types: - - completed + push: + tags: + - '*' env: REGISTRY: ghcr.io @@ -20,41 +19,54 @@ jobs: strategy: matrix: java: [ 21 ] - dockerfile-path: [Dockerfile, extra/Dockerfile] + dockerfile-path: [Dockerfile, Dockerfile-modules] include: - dockerfile-path: Dockerfile build-cmd: mvn clean package -Dcheckstyle.skip -Dmaven.test.skip=true package-name: ghcr.io/${{ github.repository }} - - dockerfile-path: extra/Dockerfile + + - dockerfile-path: Dockerfile-modules build-cmd: mvn clean package --file extra/pom.xml -Dcheckstyle.skip -Dmaven.test.skip=true package-name: ghcr.io/${{ github.repository }}-bundle steps: + - name: Check out Repository + uses: actions/checkout@v4 + - name: Set up JDK uses: actions/setup-java@v3 with: distribution: 'temurin' cache: 'maven' java-version: ${{ matrix.java }} + - name: Build .jar via Maven run: ${{ matrix.build-cmd }} - - name: Checkout repository - uses: actions/checkout@v4 + - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract metadata (tags, labels) for Docker Image id: meta uses: docker/metadata-action@v5 with: images: ${{ matrix.package-name }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ${{ matrix.dockerfile-path }} push: true + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index b34d4827eae..c1ee08ab668 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,27 +2,20 @@ name: Publish release on: push: - branches: - - master + tags: + - '*' jobs: update_release_draft: name: Publish release with notes runs-on: ubuntu-latest - if: "contains(github.event.head_commit.message, 'Prebid Server prepare release ')" steps: - - name: Extract tag from commit message - run: | - target_tag=${COMMIT_MSG#"Prebid Server prepare release "} - echo "TARGET_TAG=$target_tag" >> $GITHUB_ENV - env: - COMMIT_MSG: ${{ github.event.head_commit.message }} - name: Create and publish release uses: release-drafter/release-drafter@v5 with: config-name: release-drafter-config.yml publish: true - name: "v${{ env.TARGET_TAG }}" - tag: ${{ env.TARGET_TAG }} + name: "v${{ github.ref_name }}" + tag: ${{ github.ref_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/slack-stale-pr.yml b/.github/workflows/slack-stale-pr.yml new file mode 100644 index 00000000000..a610c3e7de9 --- /dev/null +++ b/.github/workflows/slack-stale-pr.yml @@ -0,0 +1,27 @@ +name: Post Stale PRs To Slack + +on: + # run Monday 9am and on-demand + workflow_dispatch: + schedule: + - cron: '0 9 * * 1' + +jobs: + fetch-PRs: + runs-on: ubuntu-latest + steps: + - name: Fetch pull requests + id: local + uses: paritytech/stale-pr-finder@v0.3.0 + with: + GITHUB_TOKEN: ${{ github.token }} + days-stale: 14 + ignoredLabels: "blocked" + - name: Post to a Slack channel + id: slack + uses: slackapi/slack-github-action@v1.27.1 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "${{ steps.local.outputs.message }}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/README.md b/README.md index f4c627c6c4f..9fbfe912715 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,30 @@ There are a couple of 'hello world' test requests described in sample/requests/R ## Running Docker image -Starting from PBS Java v2.9, you can download prebuilt Docker images from [GitHub Packages](https://github.com/orgs/prebid/packages?repo_name=prebid-server-java) page, -and use them instead of plain .jar files. This prebuilt images are delivered with or without extra modules. +Starting from PBS Java v3.11.0, you can download prebuilt Docker images from [GitHub Packages](https://github.com/orgs/prebid/packages?repo_name=prebid-server-java) page, +and use them instead of plain .jar files. These prebuilt images are delivered in 2 flavors: +- https://github.com/prebid/prebid-server-java/pkgs/container/prebid-server-java is a bare PBS and doesn't contain modules. +- https://github.com/prebid/prebid-server-java/pkgs/container/prebid-server-java-bundle is a "bundle" that contains PBS and all the modules. -In order to run such image correctly, you should attach PBS config file. Easiest way is to mount config file into container, +To run PBS from image correctly, you should provide the PBS config file. The easiest way is to mount the config file into the container, using [--mount or --volume (-v) Docker CLI arguments](https://docs.docker.com/engine/reference/commandline/run/). -Keep in mind, that config file should be mounted into specific location: ```/app/prebid-server/``` or ```/app/prebid-server/conf/```. +Keep in mind that the config file should be mounted into a specific location: ```/app/prebid-server/conf/``` or ```/app/prebid-server/```. + +PBS follows the regular Spring Boot config load hierarchy and type. +For simple configuration, a single `application.yaml` mounted to `/app/prebid-server/conf/` will be enough. +Please consult [Spring Externalized Configuration](https://docs.spring.io/spring-boot/reference/features/external-config.html) for all possible ways to configure PBS. + +You can also supply command-line parameters through `JAVA_OPTS` environment variable which will be appended to the `java` command before the `-jar ...` parameter. +Please pay attention to line breaks and escape them if needed. + +Example execution using sample configuration: +```shell +docker run --rm -v ./sample:/app/prebid-server/sample:ro -p 8060:8060 -p 8080:8080 ghcr.io/prebid/prebid-server-java:latest --spring.config.additional-location=sample/configs/prebid-config.yaml +``` +or +```shell +docker run --rm -v ./sample:/app/prebid-server/sample:ro -p 8060:8060 -p 8080:8080 -e JAVA_OPTS=-Dspring.config.additional-location=sample/configs/prebid-config.yaml ghcr.io/prebid/prebid-server-java:latest +``` # Documentation diff --git a/docs/admin-endpoints.md b/docs/admin-endpoints.md new file mode 100644 index 00000000000..b3176a4379c --- /dev/null +++ b/docs/admin-endpoints.md @@ -0,0 +1,209 @@ +# Admin enpoints + +Prebid Server Java offers a set of admin endpoints for managing and monitoring the server's health, configurations, and +metrics. Below is a detailed description of each endpoint, including HTTP methods, paths, parameters, and responses. + +## General settings + +Each endpoint can be either enabled or disabled by changing `admin-endpoints..enabled` toggle. Defaults to +`false`. + +Each endpoint can be configured to serve either on application port (configured via `server.http.port` setting) or +admin port (configured via `admin.port` setting) by changing `admin-endpoints..on-application-port` +setting. +By default, all admin endpoints reside on admin port. + +Each endpoint can be configured to serve on a certain path by setting `admin-endpoints..path`. + +Each endpoint can be configured to either require basic authorization or not by changing +`admin-endpoints..protected` setting, +defaults to `true`. Allowed credentials are globally configured for all admin endpoints with +`admin-endpoints.credentials.` +setting. + +## Endpoints + +1. Version info + +- Name: version +- Endpoint: Configured via `admin-endpoints.version.path` setting +- Methods: + - `GET`: + - Description: Returns the version information for the Prebid Server Java instance. + - Parameters: None + - Responses: + - 200 OK: JSON containing version details + ```json + { + "version": "x.x.x", + "revision": "commit-hash" + } + ``` + +2. Currency rates + +- Name: currency-rates +- Methods: + - `GET`: + - Description: Returns the latest information about currency rates used by server instance. + - Parameters: None + - Responses: + - 200 OK: JSON containing version details + ```json + { + "active": "true", + "source": "http://currency-source" + "fetchingIntervalNs": 200, + "lastUpdated": "02/01/2018 - 13:45:30 UTC" + ... Rates ... + } + ``` + +3. Cache notification endpoint + +- Name: storedrequest +- Methods: + - `POST`: + - Description: Updates stored requests/imps data stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": { + "": "", + ... Requests data ... + }, + "imps": { + "": "", + ... Imps data ... + } + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + - `DELETE`: + - Description: Invalidates stored requests/imps data stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": ["", ... Request names ...], + "imps": ["", ... Imp names ...] + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + +4. Amp cache notification endpoint + +- Name: storedrequest-amp +- Methods: + - `POST`: + - Description: Updates stored requests/imps data for amp, stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": { + "": "", + ... Requests data ... + }, + "imps": { + "": "", + ... Imps data ... + } + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + - `DELETE`: + - Description: Invalidates stored requests/imps data for amp, stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": ["", ... Request names ...], + "imps": ["", ... Imp names ...] + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + +5. Account cache notification endpoint + +- Name: cache-invalidation +- Methods: + - any: + - Description: Invalidates cached data for a provided account in server instance cache. + - Parameters: + - `account`: Account id. + - Responses: + - 200 OK + - 400 BAD REQUEST + + +6. Http interaction logging endpoint + +- Name: logging-httpinteraction +- Methods: + - any: + - Description: Changes request logging specification in server instance. + - Parameters: + - `endpoint`: Endpoint. Should be either: `auction` or `amp`. + - `statusCode`: Status code for logging spec. + - `account`: Account id. + - `bidder`: Bidder code. + - `limit`: Limit of requests for specification to be valid. + - Responses: + - 200 OK + - 400 BAD REQUEST +- Additional settings: + - `logging.http-interaction.max-limit` - max limit for logging specification limit. + +7. Logging level control endpoint + +- Name: logging-changelevel +- Methods: + - any: + - Description: Changes request logging level for specified amount of time in server instance. + - Parameters: + - `level`: Logging level. Should be one of: `all`, `trace`, `debug`, `info`, `warn`, `error`, `off`. + - `duration`: Duration of logging level (in millis) before reset to original one. + - Responses: + - 200 OK + - 400 BAD REQUEST +- Additional settings: + - `logging.change-level.max-duration-ms` - max duration of changed logger level. + +8. Tracer log endpoint + +- Name: tracelog +- Methods: + - any: + - Description: Adds trace logging specification for specified amount of time in server instance. + - Parameters: + - `account`: Account id. + - `bidderCode`: Bidder code. + - `level`: Log level. Should be one of: `info`, `warn`, `trace`, `error`, `fatal`, `debug`. + - `duration`: Duration of logging specification (in seconds). + - Responses: + - 200 OK + - 400 BAD REQUEST + +9. Collected metrics endpoint + +- Name: collected-metrics +- Methods: + - any: + - Description: Adds trace logging specification for specified amount of time in server instance. + - Parameters: None + - Responses: + - 200 OK: JSON containing metrics data. diff --git a/docs/application-settings.md b/docs/application-settings.md index c51febaea3e..2a20e0d8342 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -19,8 +19,13 @@ There are two ways to configure application settings: database and file. This do operational warning. - "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject the bid and log an operational warning. +- `auction.bidadjustments` - configuration JSON for default bid adjustments +- `auction.bidadjustments.mediatype.{banner, video-instream, video-outstream, audio, native, *}.{, *}.{, *}[]` - array of bid adjustment to be applied to any bid of the provided mediatype, and (`*` means ANY) +- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static) +- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment +- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment - `auction.events.enabled` - enables events for account if true -- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true. +- `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true. - `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false. - `auction.price-floors.fetch.url` - url to fetch price floors data from. - `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000. @@ -88,6 +93,7 @@ Keep in mind following restrictions: - `analytics.allow-client-details` - when true, this boolean setting allows responses to transmit the server-side analytics tags to support client-side analytics adapters. Defaults to false. - `analytics.auction-events.` - defines which channels are supported by analytics for this account - `analytics.modules..*` - space for `module-name` analytics module specific configuration, may be of any shape +- `analytics.modules..*` - a space for specific data for the analytics adapter, which may include an enabled property to control whether the adapter should be triggered, along with other adapter-specific properties. These will be merged under `ext.prebid.analytics.` in the request. - `metrics.verbosity-level` - defines verbosity level of metrics for this account, overrides `metrics.accounts` application settings configuration. - `cookie-sync.default-limit` - if the "limit" isn't specified in the `/cookie_sync` request, this is what to use - `cookie-sync.max-limit` - if the "limit" is specified in the `/cookie_sync` request, it can't be greater than this @@ -95,6 +101,7 @@ Keep in mind following restrictions: - `cookie-sync.pri` - a list of prioritized bidder codes - `cookie-sync.coop-sync.default` - if the "coopSync" value isn't specified in the `/cookie_sync` request, use this - `hooks` - configuration for Prebid Server Modules. For further details, see: https://docs.prebid.org/prebid-server/pbs-modules/index.html#2-define-an-execution-plan +- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the execution plan - `settings.geo-lookup` - enables geo lookup for account if true. Defaults to false. Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations: @@ -259,6 +266,51 @@ Here's an example YAML file containing account-specific settings: default: true ``` +## Setting Account Configuration in S3 + +This is identical to the account configuration in a file system, with the main difference that your file system is +[AWS S3](https://aws.amazon.com/de/s3/) or any S3 compatible storage, such as [MinIO](https://min.io/). + + +The general idea is that you'll place all the account-specific settings in a separate YAML file and point to that file. + +```yaml +settings: + s3: + accessKeyId: + secretAccessKey: + endpoint: # http://s3.storage.com + bucket: # prebid-application-settings + region: # if not provided AWS_GLOBAL will be used. Example value: 'eu-central-1' + accounts-dir: accounts + stored-imps-dir: stored-impressions + stored-requests-dir: stored-requests + stored-responses-dir: stored-responses + + # recommended to configure an in memory cache, but this is optional + in-memory-cache: + # example settings, tailor to your needs + cache-size: 100000 + ttl-seconds: 1200 # 20 minutes + # recommended to configure + s3-update: + refresh-rate: 900000 # Refresh every 15 minutes + timeout: 5000 +``` + +### File format + +We recommend using the `json` format for your account configuration. A minimal configuration may look like this. + +```json +{ + "id" : "979c7116-1f5a-43d4-9a87-5da3ccc4f52c", + "status" : "active" +} +``` + +This pairs nicely if you have a default configuration defined in your prebid server config under `settings.default-account-config`. + ## Setting Account Configuration in the Database In database approach account properties are stored in database table(s). diff --git a/docs/build.md b/docs/build.md index 67b0b8af26e..ed2c18b7e0a 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,9 +1,15 @@ # Build project To build the project, you will need at least -[Java 11](https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz) +[Java 21](https://whichjdk.com/) and [Maven](https://maven.apache.org/) installed. +If for whatever reason this Java reference will be stale, +you can always get the current project Java version from `pom.xml` property +```xml +... +``` + To verify the installed Java run in console: ```bash @@ -13,9 +19,9 @@ java -version which should show something like (yours may be different): ``` -openjdk version "11.0.2" 2019-01-15 -OpenJDK Runtime Environment 18.9 (build 11.0.2+9) -OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode) +openjdk version "21.0.5" 2024-10-15 LTS +OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS) +OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing) ``` Follow next steps to create JAR which can be deployed locally. diff --git a/docs/config-app.md b/docs/config-app.md index 041c1774830..ea275c2f151 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -20,12 +20,17 @@ This parameter exists to allow to change the location of the directory Vert.x wi - `server.ssl` - enable SSL/TLS support. - `server.jks-path` - path to the java keystore (if ssl is enabled). - `server.jks-password` - password for the keystore (if ssl is enabled). +- `server.cpu-load-monitoring.measurement-interval-ms` - the CPU load monitoring interval (milliseconds) ## HTTP Server - `server.max-headers-size` - set the maximum length of all headers, deprecated(use server.max-headers-size instead). - `server.ssl` - enable SSL/TLS support, deprecated(use server.ssl instead). - `server.jks-path` - path to the java keystore (if ssl is enabled), deprecated(use server.jks-path instead). - `server.jks-password` - password for the keystore (if ssl is enabled), deprecated(use server.jks-password instead). +- `server.max-initial-line-length` - set the maximum length of the initial line +- `server.idle-timeout` - set the maximum time idle connections could exist before being reaped +- `server.enable-quickack` - enables the TCP_QUICKACK option - only with linux native transport. +- `server.enable-reuseport` - set the value of reuse port - `server.http.server-instances` - how many http server instances should be created. This parameter affects how many CPU cores will be utilized by the application. Rough assumption - one http server instance will keep 1 CPU core busy. - `server.http.enabled` - if set to `true` enables http server @@ -107,11 +112,11 @@ Removes and downloads file again if depending service cant process probably corr - `auction.timeout-notification.log-sampling-rate` - instructs apply sampling when logging bidder timeout notification results ## Video -- `auction.video.stored-required` - flag forces to merge with stored request -- `auction.blocklisted-accounts` - comma separated list of blocklisted account IDs. +- `video.stored-request-required` - flag forces to merge with stored request - `video.stored-requests-timeout-ms` - timeout for stored requests fetching. -- `auction.ad-server-currency` - default currency for video auction, if its value was not specified in request. Important note: PBS uses ISO-4217 codes for the representation of currencies. +- `auction.blocklisted-accounts` - comma separated list of blocklisted account IDs. - `auction.video.escape-log-cache-regex` - regex to remove from cache debug log xml. +- `auction.ad-server-currency` - default currency for video auction, if its value was not specified in request. Important note: PBS uses ISO-4217 codes for the representation of currencies. ## Setuid - `setuid.default-timeout-ms` - default operation timeout for requests to `/setuid` endpoint. @@ -126,6 +131,7 @@ Removes and downloads file again if depending service cant process probably corr ## Vtrack - `vtrack.allow-unknown-bidder` - flag that allows servicing requests with bidders who were not configured in Prebid Server. - `vtrack.modify-vast-for-unknown-bidder` - flag that allows modifying the VAST value and adding the impression tag to it, for bidders who were not configured in Prebid Server. +- `vtrack.default-timeout-ms` - a default timeout in ms for the vtrack request ## Adapters - `adapters.*` - the section for bidder specific configuration options. @@ -160,9 +166,8 @@ Also, each bidder could have its own bidder-specific options. ## Logging - `logging.http-interaction.max-limit` - maximum value for the number of interactions to log in one take. - -## Logging - `logging.change-level.max-duration-ms` - maximum duration (in milliseconds) for which logging level could be changed. +- `logging.sampling-rate` - a percentage of messages that are logged ## Currency Converter - `currency-converter.external-rates.enabled` - if equals to `true` the currency conversion service will be enabled to fetch updated rates and convert bid currencies from external source. Also enables `/currency-rates` endpoint on admin port. @@ -213,6 +218,11 @@ Also, each bidder could have its own bidder-specific options. - `admin-endpoints.collected-metrics.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. - `admin-endpoints.collected-metrics.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` +- `admin-endpoints.logging-changelevel.enabled` - if equals to `true` the endpoint will be available. +- `admin-endpoints.logging-changelevel.path` - the server context path where the endpoint will be accessible +- `admin-endpoints.logging-changelevel.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. +- `admin-endpoints.logging-changelevel.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` + - `admin-endpoints.credentials` - user and password for access to admin endpoints if `admin-endpoints.[NAME].protected` is true`. ## Metrics @@ -264,6 +274,9 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a - `metrics.accounts.basic-verbosity` - a list of accounts for which only basic metrics will be submitted. - `metrics.accounts.detailed-verbosity` - a list of accounts for which all metrics will be submitted. +For `JVM` metrics +- `metrics.jmx.enabled` - if equals to `true` then `jvm.gc` and `jvm.memory` metrics will be submitted + ## Cache - `cache.scheme` - set the external Cache Service protocol: `http`, `https`, etc. - `cache.host` - set the external Cache Service destination in format `host:port`. @@ -278,6 +291,7 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a for particular publisher account. Overrides `cache.banner-ttl-seconds` property. - `cache.account..video-ttl-seconds` - how long (in seconds) video creative will be available in Cache Service for particular publisher account. Overrides `cache.video-ttl-seconds` property. +- `cache.default-ttl-seconds.{banner, video, audio, native}` - a default value how long (in seconds) a creative of the specific type will be available in Cache Service ## Application settings (account configuration, stored ad unit configurations, stored requests) Preconfigured application settings can be obtained from multiple data sources consequently: @@ -366,6 +380,19 @@ contain 'WHERE last_updated > ?' for MySQL and 'WHERE last_updated > $1' for Pos - `settings.in-memory-cache.database-update.refresh-rate` - refresh period in ms for stored request updates. - `settings.in-memory-cache.database-update.timeout` - timeout for obtaining stored request updates. +For S3 storage configuration +- `settings.in-memory-cache.s3-update.refresh-rate` - refresh period in ms for stored request updates in S3 +- `settings.s3.access-key-id` - an access key +- `settings.s3.secret-access-key` - a secret access key +- `settings.s3.region` - a region, AWS_GLOBAL by default +- `settings.s3.endpoint` - an endpoint +- `settings.s3.bucket` - a bucket name +- `settings.s3.force-path-style` - forces the S3 client to use path-style addressing for buckets. +- `settings.s3.accounts-dir` - a directory with stored accounts +- `settings.s3.stored-imps-dir` - a directory with stored imps +- `settings.s3.stored-requests-dir` - a directory with stored requests +- `settings.s3.stored-responses-dir` - a directory with stored responses + For targeting available next options: - `settings.targeting.truncate-attr-chars` - set the max length for names of targeting keywords (0 means no truncation). @@ -398,6 +425,7 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `gdpr.eea-countries` - comma separated list of countries in European Economic Area (EEA). - `gdpr.default-value` - determines GDPR in scope default value (if no information in request and no geolocation data). - `gdpr.host-vendor-id` - the organization running a cluster of Prebid Servers. +- `datacenter-region` - the datacenter region of a cluster of Prebid Servers - `gdpr.enabled` - gdpr feature switch. Default `true`. - `gdpr.purposes.pN.enforce-purpose` - define type of enforcement confirmation: `no`/`basic`/`full`. Default `full` - `gdpr.purposes.pN.enforce-vendors` - if equals to `true`, user must give consent to use vendors. Purposes will be omitted. Default `true` @@ -427,8 +455,30 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `geolocation.type` - set the geo location service provider, can be `maxmind` or custom provided by hosting company. - `geolocation.maxmind` - section for [MaxMind](https://www.maxmind.com) configuration as geo location service provider. - `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration. +- `geolocation.configurations[]` - a list of geo-lookup configurations for the `configuration` `geolocation.type` +- `geolocation.configurations[].address-pattern` - an address pattern for matching an IP to look up +- `geolocation.configurations[].geo-info.continent` - a continent to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.country` - a country to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.region` - a region to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.region-code` - a region code to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.city` - a city to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.metro-google` - a metro Google to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.metro-nielsen` - a metro Nielsen to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.zip` - a zip to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.connection-speed` - a connection-speed to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.lat` - a lat to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.lon` - a lon to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.time-zone` - a time zone to return on the `configuration` geo-lookup + +## IPv6 +- `ipv6.always-mask-right` - a bit mask for masking an IPv6 address of the device +- `ipv6.anon-left-mask-bits` - a bit mask for anonymizing an IPv6 address of the device +- `ipv6.private-networks` - a list of known private/local networks to skip masking of an IP address of the device ## Analytics +- `analytics.global.adapters` - Names of analytics adapters that will work for each request, except those disabled at the account level. + +For the `pubstack` analytics adapter - `analytics.pubstack.enabled` - if equals to `true` the Pubstack analytics module will be enabled. Default value is `false`. - `analytics.pubstack.endpoint` - url for reporting events and fetching configuration. - `analytics.pubstack.scopeid` - defined the scope provided by the Pubstack Support Team. @@ -438,9 +488,50 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `analytics.pubstack.buffers.count` - threshold in events count for buffer to send events - `analytics.pubstack.buffers.report-ttl-ms` - max period between two reports. +For the `greenbids` analytics adapter +- `analytics.greenbids.enabled` - if equals to `true` the Greenbids analytics module will be enabled. Default value is `false`. +- `analytics.greenbids.analytics-server-version` - a server version to add to the event +- `analytics.greenbids.analytics-server` - url for reporting events +- `analytics.greenbids.timeout-ms` - timeout in milliseconds for report requests. +- `analytics.greenbids.exploratory-sampling-split` - a sampling rate for report requests +- `analytics.greenbids.default-sampling-rate` - a default sampling rate for report requests + +For the `agma` analytics adapter +- `analytics.agma.enabled` - if equals to `true` the Agma analytics module will be enabled. Default value is `false`. +- `analytics.agma.endpoint.url` - url for reporting events +- `analytics.agma.endpoint.timeout-ms` - timeout in milliseconds for report requests. +- `analytics.agma.endpoint.gzip` - if equals to `true` the Agma analytics module enables gzip encoding. Default value is `false`. +- `analytics.agma.buffers.size-bytes` - threshold in bytes for buffer to send events. +- `analytics.agma.buffers.count` - threshold in events count for buffer to send events. +- `analytics.agma.buffers.timeout-ms` - max period between two reports. +- `analytics.agma.accounts[].code` - an account code to send with an event +- `analytics.agma.accounts[].publisher-id` - a publisher id to match an event to send +- `analytics.agma.accounts[].site-app-id` - a site or app id to match an event to send + +## Modules +- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the execution plan +- `settings.modules.require-config-to-invoke` - when enabled it requires a runtime config to exist for a module. + ## Debugging - `debug.override-token` - special string token for overriding Prebid Server account and/or adapter debug information presence in the auction response. To override (force enable) account and/or bidder adapter debug setting, a client must include `x-pbs-debug-override` HTTP header in the auction call containing same token as in the `debug.override-token` property. This will make Prebid Server ignore account `auction.debug-allow` and/or `adapters..debug.allow` properties. + +## Privacy Sandbox +- `auction.privacysandbox.topicsdomain` - the list of Sec-Browsing-Topics for the Privacy Sandbox + +## AMP +- `amp.custom-targeting` - a list of bidders that support custom targeting + +## Hooks +- `hooks.host-execution-plan` - a host execution plan for modules +- `hooks.default-account-execution-plan` - a default account execution plan + +## Price Floors Debug +- `price-floors.enabled` - enables price floors for account if true. Defaults to true. +- `price-floors.min-max-age-sec` - a price floors fetch data time to live in cache. +- `price-floors.min-period-sec` - a refresh period for fetching price floors data. +- `price-floors.min-timeout-ms` - a min timeout in ms for fetching price floors data. +- `price-floors.max-timeout-ms` - a max timeout in ms for fetching price floors data. diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md index 78728fef18a..ba7fb0ee526 100644 --- a/docs/developers/code-reviews.md +++ b/docs/developers/code-reviews.md @@ -3,33 +3,21 @@ ## Standards Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server-java/pulls). -All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge. - -Very small pull requests may be merged with just one review if they: - -1. Do not change the public API. -2. Have low risk of bugs, in the opinion of the reviewer. -3. Introduce no new features, or impact the code architecture. - -Larger pull requests must meet at least one of the following two additional requirements. - -1. Have a second approval from a core member -2. Be open for 5 business days with no new changes requested. +1. PRs that touch only adapters and modules can be approved by one reviewer before merge. +2. PRs that touch PBS-core must be reviewed and approved by at least two 'core' reviewers before merge. ## Process -New pull requests should be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) -to a core member for review within 3 business days of being opened. -That person should either approve the changes or request changes within 4 business days of being assigned. -If they're too busy, they should assign it to someone else who can review it within that timeframe. +New pull requests must be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) +to a reviewer within 5 business days of being opened. That person must either approve the changes or request changes within 5 business days of being assigned. + +If a reviewer is too busy, they should re-assign it to someone else as soon as possible so that person has enough time to take over the review and still meet the 5-day goal. Please tag the new reviewer in the PR. If you don't know who to assign it to, use the #prebid-server-java-dev Slack channel to ask for help in re-assigning. -If the changes are small, that member can merge the PR once the changes are complete. Otherwise, they should -assign the pull request to another member for a second review. +If a reviewer is going to be unavailable for more than a few days, they should update the notes column of the duty spreadsheet or drop a note about their availability into the Slack channel. -The pull request can then be merged whenever the second reviewer approves, or if 5 business days pass with no farther -changes requested by anybody, whichever comes first. +After the review, if the PR touches PBS-core, it must be assigned to a second reviewer. -## Priorities +## Review Priorities Code reviews should focus on things which cannot be validated by machines. @@ -43,3 +31,10 @@ explaining it. Are there better ways to achieve those goals? - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead! - Can the code be organized into smaller, more modular pieces? - Is there dead code which can be deleted? Or TODO comments which should be resolved? +- Look for code used by other adapters. Encourage adapter submitter to utilize common code. +- Specific bid adapter rules: + - The email contact must work and be a group, not an individual. + - Host endpoints cannot be fully dynamic. i.e. they can utilize "https://REGION.example.com", but not "https://HOST". + - They cannot _require_ a "region" parameter. Region may be an optional parameter, but must have a default. + - No direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job* + - If the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter* diff --git a/docs/developers/code-style.md b/docs/developers/code-style.md index 14704d20799..de42811030f 100644 --- a/docs/developers/code-style.md +++ b/docs/developers/code-style.md @@ -28,7 +28,7 @@ in `pom.xml` directly. It is recommended to define version of library to separate property in `pom.xml`: -``` +```xml 2.6.2 @@ -48,7 +48,7 @@ It is recommended to define version of library to separate property in `pom.xml` Do not use wildcard in imports because they hide what exactly is required by the class. -``` +```java // bad import java.util.*; @@ -61,7 +61,7 @@ import java.util.Map; Prefer to use `camelCase` naming convention for variables and methods. -``` +```java // bad String account_id = "id"; @@ -71,7 +71,7 @@ String accountId = "id"; Name of variable should be self-explanatory: -``` +```java // bad String s = resolveParamA(); @@ -83,7 +83,7 @@ This helps other developers flesh your code out better without additional questi For `Map`s it is recommended to use `To` between key and value designation: -``` +```java // bad Map map = getData(); @@ -97,7 +97,7 @@ Make data transfer object(DTO) classes immutable with static constructor. This can be achieved by using Lombok and `@Value(staticConstructor="of")`. When constructor uses multiple(more than 4) arguments, use builder instead(`@Builder`). If dto must be modified somewhere, use builders annotation `toBuilder=true` parameter and rebuild instance by calling `toBuilder()` method. -``` +```java // bad public class MyDto { @@ -138,7 +138,7 @@ final MyDto updatedDto = myDto.toBuilder().value("newValue").build(); Although Java supports the `var` keyword at the time of writing this documentation, the maintainers have chosen not to utilize it within the PBS codebase. Instead, write full variable type. -``` +```java // bad final var result = getResult(); @@ -150,7 +150,7 @@ final Data result = getResult(); Enclosing parenthesis should be placed on expression end. -``` +```java // bad methodCall( long list of arguments @@ -163,7 +163,7 @@ methodCall( This also applies for nested expressions. -``` +```java // bad methodCall( nestedCall( @@ -181,7 +181,7 @@ methodCall( Please, place methods inside a class in call order. -``` +```java // bad public interface Test { @@ -249,7 +249,7 @@ Define interface first method, then all methods that it is calling, then second Not strict, but methods with long parameters list, that cannot be placed on single line, should add empty line before body definition. -``` +```java // bad public static void method( parameters definitions) { @@ -266,7 +266,7 @@ public static void method( Use collection literals where it is possible to define and initialize collections. -``` +```java // bad final List foo = new ArrayList(); foo.add("foo"); @@ -278,7 +278,7 @@ final List foo = List.of("foo", "bar"); Also, use special methods of Collections class for empty or single-value one-line collection creation. This makes developer intention clear and code less error-prone. -``` +```java // bad return List.of(); @@ -296,7 +296,7 @@ return Collections.singletonList("foo"); It is recommended to declare variable as `final`- not strict but rather project convention to keep the code safe. -``` +```java // bad String value = "value"; @@ -308,7 +308,7 @@ final String value = "value"; Results of long ternary operators should be on separate lines: -``` +```java // bad boolean result = someVeryVeryLongConditionThatForcesLineWrap ? firstResult : secondResult; @@ -321,7 +321,7 @@ boolean result = someVeryVeryLongConditionThatForcesLineWrap Not so strict, but short ternary operations should be on one line: -``` +```java // bad boolean result = someShortCondition ? firstResult @@ -335,7 +335,7 @@ boolean result = someShortCondition ? firstResult : secondResult; Do not rely on operator precedence in boolean logic, use parenthesis instead. This will make code simpler and less error-prone. -``` +```java // bad final boolean result = a && b || c; @@ -347,7 +347,7 @@ final boolean result = (a && b) || c; Try to avoid hard-readable multiple nested method calls: -``` +```java // bad int resolvedValue = resolveValue(fetchExternalJson(url, httpClient), populateAdditionalKeys(mainKeys, keyResolver)); @@ -361,7 +361,7 @@ int resolvedValue = resolveValue(externalJson, additionalKeys); Try not to retrieve same data more than once: -``` +```java // bad if (getData() != null) { final Data resolvedData = resolveData(getData()); @@ -380,7 +380,7 @@ if (data != null) { If you're dealing with incoming data, please be sure to check if the nested object is not null before chaining. -``` +```java // bad final ExtRequestTargeting targeting = bidRequest.getExt().getPrebid().getTargeting(); @@ -400,7 +400,7 @@ We are trying to get rid of long chains of null checks, which are described in s Don't leave commented code (don't think about the future). -``` +```java // bad // String iWillUseThisLater = "never"; ``` @@ -426,7 +426,7 @@ The code should be covered over 90%. The common way for writing tests has to comply with `given-when-then` style. -``` +```java // given final BidRequest bidRequest = BidRequest.builder().id("").build(); @@ -451,7 +451,7 @@ The team decided to use name `target` for class instance under test. Unit tests should be as granular as possible. Try to split unit tests into smaller ones until this is impossible to do. -``` +```java // bad @Test public void testFooBar() { @@ -487,7 +487,7 @@ public void testBar() { This also applies to cases where same method is tested with different arguments inside single unit test. Note: This represents the replacement we have selected for parameterized testing. -``` +```java // bad @Test public void testFooFirstSecond() { @@ -527,7 +527,7 @@ It is also recommended to structure test method names with this scheme: name of method that is being tested, word `should`, what a method should return. If a method should return something based on a certain condition, add word `when` and description of a condition. -``` +```java // bad @Test public void doSomethingTest() { @@ -547,7 +547,7 @@ public void processDataShouldReturnResultWhenInputIsData() { Place data used in test as close as possible to test code. This will make tests easier to read, review and understand. -``` +```java // bad @Test public void testFoo() { @@ -576,7 +576,7 @@ This point also implies the next one. Since we are trying to improve test simplicity and readability and place test data close to tests, we decided to avoid usage of top level constants where it is possible. Instead, just inline constant values. -``` +```java // bad public class TestClass { @@ -609,7 +609,7 @@ public class TestClass { Don't use real information in tests, like existing endpoint URLs, account IDs, etc. -``` +```java // bad String ENDPOINT_URL = "https://prebid.org"; diff --git a/docs/developers/functional-tests.md b/docs/developers/functional-tests.md index c9e827b370a..523466fb0b0 100644 --- a/docs/developers/functional-tests.md +++ b/docs/developers/functional-tests.md @@ -70,7 +70,7 @@ Functional tests need to have name template **.\*Spec.groovy** **Properties:** `launchContainers` - responsible for starting the MockServer and the MySQLContainer container. Default value is false to not launch containers for unit tests. -`tests.max-container-count` - maximum number of simultaneously running PBS containers. Default value is 2. +`tests.max-container-count` - maximum number of simultaneously running PBS containers. Default value is 5. `skipFunctionalTests` - allow to skip funtional tests. Default value is false. `skipUnitTests` - allow to skip unit tests. Default value is false. diff --git a/docs/metrics.md b/docs/metrics.md index 11f2165978c..85df92bc269 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -37,6 +37,7 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. ## General auction metrics - `app_requests` - number of requests received from applications +- `debug_requests` - number of requests received (when debug mode is enabled) - `no_cookie_requests` - number of requests without `uids` cookie or with one that didn't contain at least one live UID - `request_time` - timer tracking how long did it take for Prebid Server to serve a request - `imps_requested` - number if impressions requested @@ -89,6 +90,7 @@ Following metrics are collected and submitted if account is configured with `bas Following metrics are collected and submitted if account is configured with `detailed` verbosity: - `account..requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `` broken down by type of incoming request +- `account..debug_requests` - number of requests received from account with `` broken down by type of incoming request (when debug mode is enabled) - `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` - `account..adapter..request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` - `account..adapter..bids_received` - number of bids received from `` when incoming request was from `` @@ -133,3 +135,15 @@ Following metrics are collected and submitted if account is configured with `det - `analytics..(auction|amp|video|cookie_sync|event|setuid).timeout` - number of event requests, failed with timeout cause - `analytics..(auction|amp|video|cookie_sync|event|setuid).err` - number of event requests, failed with errors - `analytics..(auction|amp|video|cookie_sync|event|setuid).badinput` - number of event requests, rejected with bad input cause + +## Modules metrics +- `modules.module..stage..hook..call` - number of times the hook is called +- `modules.module..stage..hook..duration` - timer tracking the called hook execution time +- `modules.module..stage..hook..success.(noop|update|reject|no-invocation)` - number of times the hook is called successfully with the action applied +- `modules.module..stage..hook..(failure|timeout|execution-error)` - number of times the hook execution is failed + +## Modules per-account metrics +- `account..modules.module..call` - number of times the module is called +- `account..modules.module..duration` - timer tracking the called module execution time +- `account..modules.module..success.(noop|update|reject|no-invocation)` - number of times the module is called successfully with the action applied +- `account..modules.module..failure` - number of times the module execution is failed diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 069e8c64f69..a835b8557e4 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT ../../extra/pom.xml @@ -41,6 +40,21 @@ pb-richmedia-filter ${project.version} + + org.prebid.server.hooks.modules + pb-response-correction + ${project.version} + + + org.prebid.server.hooks.modules + greenbids-real-time-data + ${project.version} + + + org.prebid.server.hooks.modules + pb-request-correction + ${project.version} + diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 77602a9c00b..92b6fbd0f73 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java index 57eac3d3620..0a7e9c7c2ea 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java @@ -3,10 +3,10 @@ import com.iab.openrtb.response.Bid; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.analytics.AppliedTo; import org.prebid.server.hooks.v1.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java index 7db1446bcce..8a65e74db63 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java @@ -18,9 +18,9 @@ import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner; import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java deleted file mode 100644 index 76fa5759644..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index 4453cb34e12..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 34beae0b73b..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java deleted file mode 100644 index 439552f562f..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java deleted file mode 100644 index 1c01790d6b8..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java index 9ec01a7cfed..f3ea0d4764e 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java @@ -2,10 +2,10 @@ import org.junit.jupiter.api.Test; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.v1.analytics.Tags; import java.util.List; diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java index 2bd6a01b993..926865781d3 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java @@ -16,15 +16,15 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsMapper; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner; import org.prebid.server.hooks.modules.com.confiant.adquality.core.RedisParser; import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -71,11 +71,7 @@ public void setUp() { @Test public void codeShouldHaveValidConfigsWhenInitialized() { - // given - - // when - - // then + // when and then assertThat(target.code()).isEqualTo("confiant-ad-quality-bid-responses-scan-hook"); } diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java index 33ad4eef240..41e63920319 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java @@ -8,11 +8,7 @@ public class ConfiantAdQualityModuleTest { @Test public void shouldHaveValidInitialConfigs() { - // given - - // when - - // then + // when and then assertThat(ConfiantAdQualityModule.CODE).isEqualTo("confiant-ad-quality"); } } diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 6116a2be58c..8559c40d7c0 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT fiftyone-devicedetection @@ -16,7 +15,6 @@ 4.4.94 - 1.2.13 @@ -33,18 +31,5 @@ device-detection ${fiftyone-device-detection.version} - - - ch.qos.logback - logback-classic - ${logback.version} - test - - - ch.qos.logback - logback-core - ${logback.version} - test - diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java index 9df4e2a0237..6a652ccf109 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java @@ -2,10 +2,10 @@ import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence; import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext; -import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java index 081177e8ca1..5c4b268cf68 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java @@ -13,9 +13,9 @@ import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.EnrichmentResult; import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.SecureHeadersRetriever; import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext; -import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java deleted file mode 100644 index ead75085974..00000000000 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model; - -import lombok.Builder; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Builder -public record InvocationResultImpl( - InvocationStatus status, - String message, - InvocationAction action, - PayloadUpdate payloadUpdate, - List errors, - List warnings, - List debugMessages, - Object moduleContext, - Tags analyticsTags -) implements InvocationResult { -} diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java index 5e3582b5297..cfd079299a0 100644 --- a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java @@ -402,7 +402,6 @@ public void callShouldReturnUpdateActionWhenFilterIsNull() { @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAuctionContext() { // given - final AuctionInvocationContext context = AuctionInvocationContextImpl.of( null, null, @@ -470,7 +469,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAuctionContext @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccount() { // given - final AuctionContext auctionContext = AuctionContext.builder().build(); final AuctionInvocationContext context = AuctionInvocationContextImpl.of( null, @@ -493,7 +491,6 @@ public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccount() { @Test public void callShouldReturnNoUpdateActionWhenNoWhitelistAndNoAccountButDeviceIdIsSet() { // given - final AuctionContext auctionContext = AuctionContext.builder().build(); final AuctionInvocationContext context = AuctionInvocationContextImpl.of( null, @@ -568,7 +565,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAccount() { @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .build()) @@ -648,7 +644,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAccountID() { @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndEmptyAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .id("") @@ -731,7 +726,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndEmptyAccountID() @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndAllowedAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .id("42") @@ -814,7 +808,6 @@ public void callShouldReturnUpdateActionWhenWhitelistFilledAndAllowedAccountID() @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNotAllowedAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .id("29") diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml new file mode 100644 index 00000000000..5eae149759c --- /dev/null +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.prebid.server.hooks.modules + all-modules + 3.18.0-SNAPSHOT + + + greenbids-real-time-data + + greenbids-real-time-data + Greenbids Real Time Data + + + + com.github.ua-parser + uap-java + 1.6.1 + + + + com.microsoft.onnxruntime + onnxruntime + 1.16.1 + + + + com.google.cloud + google-cloud-storage + 2.41.0 + + + + diff --git a/extra/modules/greenbids-real-time-data/src/lombok.config b/extra/modules/greenbids-real-time-data/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java new file mode 100644 index 00000000000..a40c98ebb25 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java @@ -0,0 +1,55 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import com.maxmind.geoip2.DatabaseReader; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.vertx.Initializable; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; + +public class DatabaseReaderFactory implements Initializable { + + private final String geoLiteCountryUrl; + + private final Vertx vertx; + + private final AtomicReference databaseReaderRef = new AtomicReference<>(); + + public DatabaseReaderFactory(String geoLitCountryUrl, Vertx vertx) { + this.geoLiteCountryUrl = geoLitCountryUrl; + this.vertx = vertx; + } + + @Override + public void initialize(Promise initializePromise) { + + vertx.executeBlocking(() -> { + try { + final URL url = new URL(geoLiteCountryUrl); + final Path databasePath = Files.createTempFile("GeoLite2-Country", ".mmdb"); + + try (InputStream inputStream = url.openStream(); + FileOutputStream outputStream = new FileOutputStream(databasePath.toFile())) { + inputStream.transferTo(outputStream); + } + + databaseReaderRef.set(new DatabaseReader.Builder(databasePath.toFile()).build()); + } catch (IOException e) { + throw new PreBidException("Failed to initialize DatabaseReader from URL", e); + } + return null; + }).mapEmpty() + .onComplete(initializePromise); + } + + public DatabaseReader getDatabaseReader() { + return databaseReaderRef.get(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java new file mode 100644 index 00000000000..959352d1908 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java @@ -0,0 +1,134 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import io.vertx.core.Vertx; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.GreenbidsRealTimeDataProcessedAuctionRequestHook; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@ConditionalOnProperty(prefix = "hooks." + GreenbidsRealTimeDataModule.CODE, name = "enabled", havingValue = "true") +@Configuration +@EnableConfigurationProperties(GreenbidsRealTimeDataProperties.class) +public class GreenbidsRealTimeDataConfiguration { + + @Bean + DatabaseReaderFactory databaseReaderFactory(GreenbidsRealTimeDataProperties properties, Vertx vertx) { + return new DatabaseReaderFactory(properties.getGeoLiteCountryPath(), vertx); + } + + @Bean + GreenbidsInferenceDataService greenbidsInferenceDataService(DatabaseReaderFactory databaseReaderFactory) { + return new GreenbidsInferenceDataService( + databaseReaderFactory, ObjectMapperProvider.mapper()); + } + + @Bean + GreenbidsRealTimeDataModule greenbidsRealTimeDataModule( + FilterService filterService, + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds, + GreenbidsInferenceDataService greenbidsInferenceDataService, + GreenbidsInvocationService greenbidsInvocationService) { + + return new GreenbidsRealTimeDataModule(List.of( + new GreenbidsRealTimeDataProcessedAuctionRequestHook( + ObjectMapperProvider.mapper(), + filterService, + onnxModelRunnerWithThresholds, + greenbidsInferenceDataService, + greenbidsInvocationService))); + } + + @Bean + FilterService filterService() { + return new FilterService(); + } + + @Bean + Storage storage(GreenbidsRealTimeDataProperties properties) { + return StorageOptions.newBuilder() + .setProjectId(properties.getGoogleCloudGreenbidsProject()).build().getService(); + } + + @Bean + OnnxModelRunnerFactory onnxModelRunnerFactory() { + return new OnnxModelRunnerFactory(); + } + + @Bean + ThrottlingThresholdsFactory throttlingThresholdsFactory() { + return new ThrottlingThresholdsFactory(); + } + + @Bean + ModelCache modelCache( + GreenbidsRealTimeDataProperties properties, + Vertx vertx, + Storage storage, + OnnxModelRunnerFactory onnxModelRunnerFactory) { + + final Cache modelCacheWithExpiration = Caffeine.newBuilder() + .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES) + .build(); + + return new ModelCache( + storage, + properties.getGcsBucketName(), + modelCacheWithExpiration, + properties.getOnnxModelCacheKeyPrefix(), + vertx, + onnxModelRunnerFactory); + } + + @Bean + ThresholdCache thresholdCache( + GreenbidsRealTimeDataProperties properties, + Vertx vertx, + Storage storage, + ThrottlingThresholdsFactory throttlingThresholdsFactory) { + + final Cache thresholdsCacheWithExpiration = Caffeine.newBuilder() + .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES) + .build(); + + return new ThresholdCache( + storage, + properties.getGcsBucketName(), + ObjectMapperProvider.mapper(), + thresholdsCacheWithExpiration, + properties.getThresholdsCacheKeyPrefix(), + vertx, + throttlingThresholdsFactory); + } + + @Bean + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds( + ModelCache modelCache, + ThresholdCache thresholdCache) { + + return new OnnxModelRunnerWithThresholds(modelCache, thresholdCache); + } + + @Bean + GreenbidsInvocationService greenbidsInvocationService() { + return new GreenbidsInvocationService(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java new file mode 100644 index 00000000000..b2e5bdcfeb8 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.List; + +public class GreenbidsRealTimeDataModule implements Module { + + public static final String CODE = "greenbids-real-time-data"; + + private final List> hooks; + + public GreenbidsRealTimeDataModule(List> hooks) { + this.hooks = hooks; + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java new file mode 100644 index 00000000000..86736a6011f --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "hooks.modules." + GreenbidsRealTimeDataModule.CODE) +@Data +public class GreenbidsRealTimeDataProperties { + + String googleCloudGreenbidsProject; + + String geoLiteCountryPath; + + String gcsBucketName; + + Integer cacheExpirationMinutes; + + String onnxModelCacheKeyPrefix; + + String thresholdsCacheKeyPrefix; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java new file mode 100644 index 00000000000..094c2d18df1 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java @@ -0,0 +1,123 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OnnxValue; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class FilterService { + + public Map> filterBidders( + OnnxModelRunner onnxModelRunner, + List throttlingMessages, + Double threshold) { + + final OrtSession.Result results; + try { + final String[][] throttlingInferenceRows = convertToArray(throttlingMessages); + results = onnxModelRunner.runModel(throttlingInferenceRows); + return processModelResults(results, throttlingMessages, threshold); + } catch (OrtException e) { + throw new PreBidException("Exception during model inference: ", e); + } + } + + private static String[][] convertToArray(List messages) { + return messages.stream() + .map(message -> new String[]{ + message.getBrowser(), + message.getBidder(), + message.getAdUnitCode(), + message.getCountry(), + message.getHostname(), + message.getDevice(), + message.getHourBucket(), + message.getMinuteQuadrant()}) + .toArray(String[][]::new); + } + + private Map> processModelResults( + OrtSession.Result results, + List throttlingMessages, + Double threshold) { + + validateThrottlingMessages(throttlingMessages); + + return StreamSupport.stream(results.spliterator(), false) + .peek(FilterService::validateOnnxTensor) + .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities")) + .map(Map.Entry::getValue) + .map(OnnxTensor.class::cast) + .peek(tensor -> validateTensorSize(tensor, throttlingMessages.size())) + .map(tensor -> extractAndProcessProbabilities(tensor, throttlingMessages, threshold)) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static void validateThrottlingMessages(List throttlingMessages) { + if (throttlingMessages == null || CollectionUtils.isEmpty(throttlingMessages)) { + throw new PreBidException("throttlingMessages cannot be null or empty"); + } + } + + private static void validateOnnxTensor(Map.Entry onnxItem) { + if (!(onnxItem.getValue() instanceof OnnxTensor)) { + throw new PreBidException("Expected OnnxTensor for 'probabilities', but found: " + + onnxItem.getValue().getClass().getName()); + } + } + + private static void validateTensorSize(OnnxTensor tensor, int expectedSize) { + final long[] tensorShape = tensor.getInfo().getShape(); + if (tensorShape.length == 0 || tensorShape[0] != expectedSize) { + throw new PreBidException("Mismatch between tensor size and throttlingMessages size"); + } + } + + private Map> extractAndProcessProbabilities( + OnnxTensor tensor, + List throttlingMessages, + Double threshold) { + + try { + final float[][] probabilities = extractProbabilitiesValues(tensor); + return processProbabilities(probabilities, throttlingMessages, threshold); + } catch (OrtException e) { + throw new PreBidException("Exception when extracting proba from OnnxTensor: ", e); + } + } + + private float[][] extractProbabilitiesValues(OnnxTensor tensor) throws OrtException { + return (float[][]) tensor.getValue(); + } + + private Map> processProbabilities( + float[][] probabilities, + List throttlingMessages, + Double threshold) { + + final Map> result = new HashMap<>(); + + for (int i = 0; i < probabilities.length; i++) { + final ThrottlingMessage message = throttlingMessages.get(i); + final String impId = message.getAdUnitCode(); + final String bidder = message.getBidder(); + final boolean isKeptInAuction = probabilities[i][1] > threshold; + result.computeIfAbsent(impId, k -> new HashMap<>()).put(bidder, isKeptInAuction); + } + + return result; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java new file mode 100644 index 00000000000..3bd3e37b859 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java @@ -0,0 +1,184 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.Country; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; + +import java.io.IOException; +import java.net.InetAddress; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class GreenbidsInferenceDataService { + + private final DatabaseReaderFactory databaseReaderFactory; + + private final ObjectMapper mapper; + + public GreenbidsInferenceDataService(DatabaseReaderFactory dbReaderFactory, ObjectMapper mapper) { + this.databaseReaderFactory = Objects.requireNonNull(dbReaderFactory); + this.mapper = Objects.requireNonNull(mapper); + } + + public List extractThrottlingMessagesFromBidRequest(BidRequest bidRequest) { + final GreenbidsUserAgent userAgent = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getUa) + .map(GreenbidsUserAgent::new) + .orElse(null); + + return extractThrottlingMessages(bidRequest, userAgent); + } + + private List extractThrottlingMessages( + BidRequest bidRequest, + GreenbidsUserAgent greenbidsUserAgent) { + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer hourBucket = timestamp.getHour(); + final Integer minuteQuadrant = (timestamp.getMinute() / 15) + 1; + + final String hostname = bidRequest.getSite().getDomain(); + final List imps = bidRequest.getImp(); + + return imps.stream() + .map(imp -> extractMessagesForImp( + imp, + bidRequest, + greenbidsUserAgent, + hostname, + hourBucket, + minuteQuadrant)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private List extractMessagesForImp( + Imp imp, + BidRequest bidRequest, + GreenbidsUserAgent greenbidsUserAgent, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final String impId = imp.getId(); + final ObjectNode impExt = imp.getExt(); + final JsonNode bidderNode = extImpPrebid(impExt.get("prebid")).getBidder(); + final String ip = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getIp) + .orElse(null); + final String countryFromIp = getCountry(ip); + return createThrottlingMessages( + bidderNode, + impId, + greenbidsUserAgent, + countryFromIp, + hostname, + hourBucket, + minuteQuadrant); + } + + private String getCountry(String ip) { + if (ip == null) { + return null; + } + + final DatabaseReader databaseReader = databaseReaderFactory.getDatabaseReader(); + try { + final InetAddress inetAddress = InetAddress.getByName(ip); + final CountryResponse response = databaseReader.country(inetAddress); + final Country country = response.getCountry(); + return country.getName(); + } catch (IOException | GeoIp2Exception e) { + throw new PreBidException("Failed to fetch country from geoLite DB", e); + } + } + + private List createThrottlingMessages( + JsonNode bidderNode, + String impId, + GreenbidsUserAgent greenbidsUserAgent, + String countryFromIp, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final List throttlingImpMessages = new ArrayList<>(); + + if (!bidderNode.isObject()) { + return throttlingImpMessages; + } + + final ObjectNode bidders = (ObjectNode) bidderNode; + final Iterator fieldNames = bidders.fieldNames(); + while (fieldNames.hasNext()) { + final String bidderName = fieldNames.next(); + throttlingImpMessages.add(buildThrottlingMessage( + bidderName, + impId, + greenbidsUserAgent, + countryFromIp, + hostname, + hourBucket, + minuteQuadrant)); + } + + return throttlingImpMessages; + } + + private ThrottlingMessage buildThrottlingMessage( + String bidderName, + String impId, + GreenbidsUserAgent greenbidsUserAgent, + String countryFromIp, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final String browser = Optional.ofNullable(greenbidsUserAgent) + .map(GreenbidsUserAgent::getBrowser) + .orElse(StringUtils.EMPTY); + + final String device = Optional.ofNullable(greenbidsUserAgent) + .map(GreenbidsUserAgent::getDevice) + .orElse(StringUtils.EMPTY); + + return ThrottlingMessage.builder() + .browser(browser) + .bidder(StringUtils.defaultString(bidderName)) + .adUnitCode(StringUtils.defaultString(impId)) + .country(StringUtils.defaultString(countryFromIp)) + .hostname(StringUtils.defaultString(hostname)) + .device(device) + .hourBucket(StringUtils.defaultString(hourBucket.toString())) + .minuteQuadrant(StringUtils.defaultString(minuteQuadrant.toString())) + .build(); + } + + private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { + try { + return mapper.treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java new file mode 100644 index 00000000000..67d42d47bc2 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java @@ -0,0 +1,120 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public class GreenbidsInvocationService { + + private static final int RANGE_16_BIT_INTEGER_DIVISION_BASIS = 0x10000; + + public GreenbidsInvocationResult createGreenbidsInvocationResult( + Partner partner, + BidRequest bidRequest, + Map> impsBiddersFilterMap) { + + final String greenbidsId = UUID.randomUUID().toString(); + final boolean isExploration = isExploration(partner, greenbidsId); + + final BidRequest updatedBidRequest = isExploration + ? bidRequest + : bidRequest.toBuilder() + .imp(updateImps(bidRequest, impsBiddersFilterMap)) + .build(); + final InvocationAction invocationAction = isExploration + ? InvocationAction.no_action + : InvocationAction.update; + final Map> impsBiddersFilterMapToAnalyticsTag = isExploration + ? keepAllBiddersForAnalyticsResult(impsBiddersFilterMap) + : impsBiddersFilterMap; + final Map ort2ImpExtResultMap = createOrtb2ImpExtForImps( + bidRequest, impsBiddersFilterMapToAnalyticsTag, greenbidsId, isExploration); + final AnalyticsResult analyticsResult = AnalyticsResult.of( + "success", ort2ImpExtResultMap, null, null); + + return GreenbidsInvocationResult.of(updatedBidRequest, invocationAction, analyticsResult); + } + + private Boolean isExploration(Partner partner, String greenbidsId) { + final int hashInt = Integer.parseInt( + greenbidsId.substring(greenbidsId.length() - 4), 16); + return hashInt < partner.getExplorationRate() * RANGE_16_BIT_INTEGER_DIVISION_BASIS; + } + + private List updateImps(BidRequest bidRequest, Map> impsBiddersFilterMap) { + return bidRequest.getImp().stream() + .map(imp -> updateImp(imp, impsBiddersFilterMap.get(imp.getId()))) + .toList(); + } + + private Imp updateImp(Imp imp, Map bidderFilterMap) { + return imp.toBuilder() + .ext(updateImpExt(imp.getExt(), bidderFilterMap)) + .build(); + } + + private ObjectNode updateImpExt(ObjectNode impExt, Map bidderFilterMap) { + final ObjectNode updatedExt = impExt.deepCopy(); + Optional.ofNullable((ObjectNode) updatedExt.get("prebid")) + .map(prebidNode -> (ObjectNode) prebidNode.get("bidder")) + .ifPresent(bidderNode -> + bidderFilterMap.entrySet().stream() + .filter(entry -> !entry.getValue()) + .map(Map.Entry::getKey) + .forEach(bidderNode::remove)); + return updatedExt; + } + + private Map> keepAllBiddersForAnalyticsResult( + Map> impsBiddersFilterMap) { + + return impsBiddersFilterMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> true)))); + } + + private Map createOrtb2ImpExtForImps( + BidRequest bidRequest, + Map> impsBiddersFilterMap, + String greenbidsId, + Boolean isExploration) { + + return bidRequest.getImp().stream() + .collect(Collectors.toMap( + Imp::getId, + imp -> createOrtb2ImpExt(imp, impsBiddersFilterMap, greenbidsId, isExploration))); + } + + private Ortb2ImpExtResult createOrtb2ImpExt( + Imp imp, + Map> impsBiddersFilterMap, + String greenbidsId, + Boolean isExploration) { + + final String tid = Optional.ofNullable(imp) + .map(Imp::getExt) + .map(impExt -> impExt.get("tid")) + .map(JsonNode::asText) + .orElse(StringUtils.EMPTY); + final Map impBiddersFilterMap = impsBiddersFilterMap.get(imp.getId()); + final ExplorationResult explorationResult = ExplorationResult.of( + greenbidsId, impBiddersFilterMap, isExploration); + return Ortb2ImpExtResult.of(explorationResult, tid); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java new file mode 100644 index 00000000000..b7450d71560 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import org.apache.commons.lang3.StringUtils; +import ua_parser.Client; +import ua_parser.Device; +import ua_parser.OS; +import ua_parser.Parser; +import ua_parser.UserAgent; + +import java.util.Optional; +import java.util.Set; + +public class GreenbidsUserAgent { + + public static final Set PC_OS_FAMILIES = Set.of( + "Windows 95", "Windows 98", "Solaris"); + + private static final Parser UA_PARSER = new Parser(); + + private final String userAgentString; + + private final UserAgent userAgent; + + private final Device device; + + private final OS os; + + public GreenbidsUserAgent(String userAgentString) { + this.userAgentString = userAgentString; + final Client client = UA_PARSER.parse(userAgentString); + this.userAgent = client.userAgent; + this.device = client.device; + this.os = client.os; + } + + public String getDevice() { + return Optional.ofNullable(device) + .map(device -> isPC() ? "PC" : device.family) + .orElse(StringUtils.EMPTY); + } + + public String getBrowser() { + return Optional.ofNullable(userAgent) + .filter(userAgent -> !"Other".equals(userAgent.family) && StringUtils.isNoneBlank(userAgent.family)) + .map(ua -> "%s %s".formatted(ua.family, StringUtils.defaultString(userAgent.major)).trim()) + .orElse(StringUtils.EMPTY); + } + + private boolean isPC() { + final String osFamily = osFamily(); + return Optional.ofNullable(userAgentString) + .map(userAgent -> userAgent.contains("Windows NT") + || PC_OS_FAMILIES.contains(osFamily) + || ("Windows".equals(osFamily) && "ME".equals(osMajor())) + || ("Mac OS X".equals(osFamily) && !userAgent.contains("Silk")) + || (userAgent.contains("Linux") && userAgent.contains("X11"))) + .orElse(false); + } + + private String osFamily() { + return Optional.ofNullable(os).map(os -> os.family).orElse(StringUtils.EMPTY); + } + + private String osMajor() { + return Optional.ofNullable(os).map(os -> os.major).orElse(StringUtils.EMPTY); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java new file mode 100644 index 00000000000..01087287d44 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java @@ -0,0 +1,96 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ModelCache { + + private static final Logger logger = LoggerFactory.getLogger(ModelCache.class); + + private final String gcsBucketName; + + private final Cache cache; + + private final Storage storage; + + private final String onnxModelCacheKeyPrefix; + + private final AtomicBoolean isFetching; + + private final Vertx vertx; + + private final OnnxModelRunnerFactory onnxModelRunnerFactory; + + public ModelCache( + Storage storage, + String gcsBucketName, + Cache cache, + String onnxModelCacheKeyPrefix, + Vertx vertx, + OnnxModelRunnerFactory onnxModelRunnerFactory) { + this.gcsBucketName = Objects.requireNonNull(gcsBucketName); + this.cache = Objects.requireNonNull(cache); + this.storage = Objects.requireNonNull(storage); + this.onnxModelCacheKeyPrefix = Objects.requireNonNull(onnxModelCacheKeyPrefix); + this.isFetching = new AtomicBoolean(false); + this.vertx = Objects.requireNonNull(vertx); + this.onnxModelRunnerFactory = Objects.requireNonNull(onnxModelRunnerFactory); + } + + public Future get(String onnxModelPath, String pbuid) { + final String cacheKey = onnxModelCacheKeyPrefix + pbuid; + final OnnxModelRunner cachedOnnxModelRunner = cache.getIfPresent(cacheKey); + + if (cachedOnnxModelRunner != null) { + return Future.succeededFuture(cachedOnnxModelRunner); + } + + if (isFetching.compareAndSet(false, true)) { + try { + return fetchAndCacheModelRunner(onnxModelPath, cacheKey); + } finally { + isFetching.set(false); + } + } + + return Future.failedFuture("ModelRunner fetching in progress. Skip current request"); + } + + private Future fetchAndCacheModelRunner(String onnxModelPath, String cacheKey) { + return vertx.executeBlocking(() -> getBlob(onnxModelPath)) + .map(this::loadModelRunner) + .onSuccess(onnxModelRunner -> cache.put(cacheKey, onnxModelRunner)) + .onFailure(error -> logger.error("Failed to fetch ONNX model")); + } + + private Blob getBlob(String onnxModelPath) { + try { + return Optional.ofNullable(storage.get(gcsBucketName)) + .map(bucket -> bucket.get(onnxModelPath)) + .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName)); + } catch (StorageException e) { + throw new PreBidException("Error accessing GCS artefact for model: ", e); + } + } + + private OnnxModelRunner loadModelRunner(Blob blob) { + try { + final byte[] onnxModelBytes = blob.getContent(); + return onnxModelRunnerFactory.create(onnxModelBytes); + } catch (OrtException e) { + throw new PreBidException("Failed to convert blob to ONNX model", e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java new file mode 100644 index 00000000000..d5570f30272 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OrtEnvironment; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; + +import java.util.Collections; + +public class OnnxModelRunner { + + private static final OrtEnvironment ENVIRONMENT = OrtEnvironment.getEnvironment(); + + private final OrtSession session; + + public OnnxModelRunner(byte[] onnxModelBytes) throws OrtException { + session = ENVIRONMENT.createSession(onnxModelBytes, new OrtSession.SessionOptions()); + } + + public OrtSession.Result runModel(String[][] throttlingInferenceRow) throws OrtException { + final OnnxTensor inputTensor = OnnxTensor.createTensor(ENVIRONMENT, throttlingInferenceRow); + return session.run(Collections.singletonMap("input", inputTensor)); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java new file mode 100644 index 00000000000..b6082cf3e12 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; + +public class OnnxModelRunnerFactory { + + public OnnxModelRunner create(byte[] bytes) throws OrtException { + return new OnnxModelRunner(bytes); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java new file mode 100644 index 00000000000..adbc1e17b2c --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import io.vertx.core.Future; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; + +import java.util.Objects; + +public class OnnxModelRunnerWithThresholds { + + private final ModelCache modelCache; + + private final ThresholdCache thresholdCache; + + public OnnxModelRunnerWithThresholds( + ModelCache modelCache, + ThresholdCache thresholdCache) { + this.modelCache = Objects.requireNonNull(modelCache); + this.thresholdCache = Objects.requireNonNull(thresholdCache); + } + + public Future retrieveOnnxModelRunner(Partner partner) { + final String onnxModelPath = "models_pbuid=" + partner.getPbuid() + ".onnx"; + return modelCache.get(onnxModelPath, partner.getPbuid()); + } + + public Future retrieveThreshold(Partner partner) { + final String thresholdJsonPath = "thresholds_pbuid=" + partner.getPbuid() + ".json"; + return thresholdCache.get(thresholdJsonPath, partner.getPbuid()) + .map(partner::getThreshold); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java new file mode 100644 index 00000000000..44eb3d1403a --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java @@ -0,0 +1,102 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ThresholdCache { + + private static final Logger logger = LoggerFactory.getLogger(ThresholdCache.class); + + private final String gcsBucketName; + + private final Cache cache; + + private final Storage storage; + + private final ObjectMapper mapper; + + private final String thresholdsCacheKeyPrefix; + + private final AtomicBoolean isFetching; + + private final Vertx vertx; + + private final ThrottlingThresholdsFactory throttlingThresholdsFactory; + + public ThresholdCache( + Storage storage, + String gcsBucketName, + ObjectMapper mapper, + Cache cache, + String thresholdsCacheKeyPrefix, + Vertx vertx, + ThrottlingThresholdsFactory throttlingThresholdsFactory) { + this.gcsBucketName = Objects.requireNonNull(gcsBucketName); + this.cache = Objects.requireNonNull(cache); + this.storage = Objects.requireNonNull(storage); + this.mapper = Objects.requireNonNull(mapper); + this.thresholdsCacheKeyPrefix = Objects.requireNonNull(thresholdsCacheKeyPrefix); + this.isFetching = new AtomicBoolean(false); + this.vertx = Objects.requireNonNull(vertx); + this.throttlingThresholdsFactory = Objects.requireNonNull(throttlingThresholdsFactory); + } + + public Future get(String thresholdJsonPath, String pbuid) { + final String cacheKey = thresholdsCacheKeyPrefix + pbuid; + final ThrottlingThresholds cachedThrottlingThresholds = cache.getIfPresent(cacheKey); + + if (cachedThrottlingThresholds != null) { + return Future.succeededFuture(cachedThrottlingThresholds); + } + + if (isFetching.compareAndSet(false, true)) { + try { + return fetchAndCacheThrottlingThresholds(thresholdJsonPath, cacheKey); + } finally { + isFetching.set(false); + } + } + + return Future.failedFuture("ThrottlingThresholds fetching in progress. Skip current request"); + } + + private Future fetchAndCacheThrottlingThresholds(String thresholdJsonPath, String cacheKey) { + return vertx.executeBlocking(() -> getBlob(thresholdJsonPath)) + .map(this::loadThrottlingThresholds) + .onSuccess(thresholds -> cache.put(cacheKey, thresholds)) + .onFailure(error -> logger.error("Failed to fetch thresholds")); + } + + private Blob getBlob(String thresholdJsonPath) { + try { + return Optional.ofNullable(storage.get(gcsBucketName)) + .map(bucket -> bucket.get(thresholdJsonPath)) + .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName)); + } catch (StorageException e) { + throw new PreBidException("Error accessing GCS artefact for threshold: ", e); + } + } + + private ThrottlingThresholds loadThrottlingThresholds(Blob blob) { + try { + final byte[] jsonBytes = blob.getContent(); + return throttlingThresholdsFactory.create(jsonBytes, mapper); + } catch (IOException e) { + throw new PreBidException("Failed to load throttling thresholds json", e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java new file mode 100644 index 00000000000..e7ac4a6a4a9 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; + +import java.io.IOException; + +public class ThrottlingThresholdsFactory { + + public ThrottlingThresholds create(byte[] bytes, ObjectMapper mapper) throws IOException { + final JsonNode thresholdsJsonNode = mapper.readTree(bytes); + return mapper.treeToValue(thresholdsJsonNode, ThrottlingThresholds.class); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java new file mode 100644 index 00000000000..2be7c1887e8 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.IntStream; + +@Value(staticConstructor = "of") +public class Partner { + + String pbuid; + + @JsonProperty("targetTpr") + Double targetTpr; + + @JsonProperty("explorationRate") + Double explorationRate; + + public Double getThreshold(ThrottlingThresholds throttlingThresholds) { + final List truePositiveRates = throttlingThresholds.getTpr(); + final List thresholds = throttlingThresholds.getThresholds(); + + final int minSize = Math.min(truePositiveRates.size(), thresholds.size()); + + return IntStream.range(0, minSize) + .filter(i -> truePositiveRates.get(i) >= targetTpr) + .mapToObj(thresholds::get) + .max(Comparator.naturalOrder()) + .orElse(0.0); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java new file mode 100644 index 00000000000..8acb6718936 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class ThrottlingMessage { + + String browser; + + String bidder; + + String adUnitCode; + + String country; + + String hostname; + + String device; + + String hourBucket; + + String minuteQuadrant; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java new file mode 100644 index 00000000000..ccd6594ee38 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ThrottlingThresholds { + + List thresholds; + + List tpr; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java new file mode 100644 index 00000000000..9d175b5b4b3 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result; + +import lombok.Value; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class AnalyticsResult { + + String status; + + Map values; + + String bidder; + + String impId; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java new file mode 100644 index 00000000000..0aff44ceaec --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result; + +import com.iab.openrtb.request.BidRequest; +import lombok.Value; +import org.prebid.server.hooks.v1.InvocationAction; + +@Value(staticConstructor = "of") +public class GreenbidsInvocationResult { + + BidRequest updatedBidRequest; + + InvocationAction invocationAction; + + AnalyticsResult analyticsResult; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..3b677a78a18 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java @@ -0,0 +1,210 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class GreenbidsRealTimeDataProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "greenbids-real-time-data-processed-auction-request"; + private static final String ACTIVITY = "greenbids-filter"; + private static final String SUCCESS_STATUS = "success"; + private static final String BID_REQUEST_ANALYTICS_EXTENSION_NAME = "greenbids-rtd"; + + private final ObjectMapper mapper; + private final FilterService filterService; + private final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds; + private final GreenbidsInferenceDataService greenbidsInferenceDataService; + private final GreenbidsInvocationService greenbidsInvocationService; + + public GreenbidsRealTimeDataProcessedAuctionRequestHook( + ObjectMapper mapper, + FilterService filterService, + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds, + GreenbidsInferenceDataService greenbidsInferenceDataService, + GreenbidsInvocationService greenbidsInvocationService) { + this.mapper = Objects.requireNonNull(mapper); + this.filterService = Objects.requireNonNull(filterService); + this.onnxModelRunnerWithThresholds = Objects.requireNonNull(onnxModelRunnerWithThresholds); + this.greenbidsInferenceDataService = Objects.requireNonNull(greenbidsInferenceDataService); + this.greenbidsInvocationService = Objects.requireNonNull(greenbidsInvocationService); + } + + @Override + public Future> call( + AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final AuctionContext auctionContext = invocationContext.auctionContext(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Partner partner = parseBidRequestExt(bidRequest); + + if (partner == null) { + return Future.succeededFuture(toInvocationResult( + bidRequest, null, InvocationAction.no_action)); + } + + return Future.all( + onnxModelRunnerWithThresholds.retrieveOnnxModelRunner(partner), + onnxModelRunnerWithThresholds.retrieveThreshold(partner)) + .compose(compositeFuture -> toInvocationResult( + bidRequest, + partner, + compositeFuture.resultAt(0), + compositeFuture.resultAt(1))) + .recover(throwable -> Future.succeededFuture(toInvocationResult( + bidRequest, null, InvocationAction.no_action))); + } + + private Partner parseBidRequestExt(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .filter(this::isNotEmptyObjectNode) + .map(analytics -> (ObjectNode) analytics.get(BID_REQUEST_ANALYTICS_EXTENSION_NAME)) + .map(this::toPartner) + .orElse(null); + } + + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + + private Partner toPartner(ObjectNode adapterNode) { + try { + return mapper.treeToValue(adapterNode, Partner.class); + } catch (JsonProcessingException e) { + return null; + } + } + + private Future> toInvocationResult( + BidRequest bidRequest, + Partner partner, + OnnxModelRunner onnxModelRunner, + Double threshold) { + + final Map> impsBiddersFilterMap; + try { + final List throttlingMessages = greenbidsInferenceDataService + .extractThrottlingMessagesFromBidRequest(bidRequest); + + impsBiddersFilterMap = filterService.filterBidders( + onnxModelRunner, + throttlingMessages, + threshold); + } catch (PreBidException e) { + return Future.succeededFuture(toInvocationResult( + bidRequest, null, InvocationAction.no_action)); + } + + final GreenbidsInvocationResult greenbidsInvocationResult = greenbidsInvocationService + .createGreenbidsInvocationResult(partner, bidRequest, impsBiddersFilterMap); + + return Future.succeededFuture(toInvocationResult( + greenbidsInvocationResult.getUpdatedBidRequest(), + greenbidsInvocationResult.getAnalyticsResult(), + greenbidsInvocationResult.getInvocationAction())); + } + + private InvocationResult toInvocationResult( + BidRequest bidRequest, + AnalyticsResult analyticsResult, + InvocationAction action) { + + final List analyticsResults = analyticsResult != null + ? Collections.singletonList(analyticsResult) + : Collections.emptyList(); + + return switch (action) { + case InvocationAction.update -> InvocationResultImpl + .builder() + .status(InvocationStatus.success) + .action(action) + .payloadUpdate(payload -> AuctionRequestPayloadImpl.of(bidRequest)) + .analyticsTags(toAnalyticsTags(analyticsResults)) + .build(); + default -> InvocationResultImpl + .builder() + .status(InvocationStatus.success) + .action(action) + .analyticsTags(toAnalyticsTags(analyticsResults)) + .build(); + }; + } + + private Tags toAnalyticsTags(List analyticsResults) { + if (CollectionUtils.isEmpty(analyticsResults)) { + return null; + } + + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY, + SUCCESS_STATUS, + toResults(analyticsResults)))); + } + + private List toResults(List analyticsResults) { + return analyticsResults.stream() + .map(this::toResult) + .toList(); + } + + private Result toResult(AnalyticsResult analyticsResult) { + return ResultImpl.of( + analyticsResult.getStatus(), + toObjectNode(analyticsResult.getValues()), + AppliedToImpl.builder() + .bidders(Collections.singletonList(analyticsResult.getBidder())) + .impIds(Collections.singletonList(analyticsResult.getImpId())) + .build()); + } + + private ObjectNode toObjectNode(Map values) { + return values != null ? mapper.valueToTree(values) : null; + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java new file mode 100644 index 00000000000..0a3ab82b9cd --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java @@ -0,0 +1,177 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OnnxValue; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import ai.onnxruntime.TensorInfo; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FilterServiceTest { + + @Mock + private OnnxModelRunner onnxModelRunnerMock; + + @Mock + private OrtSession.Result results; + + @Mock + private OnnxTensor onnxTensor; + + @Mock + private TensorInfo tensorInfo; + + @Mock + private OnnxValue onnxValue; + + private final FilterService target = new FilterService(); + + @Test + public void filterBiddersShouldReturnFilteredBiddersWhenValidThrottlingMessagesProvided() + throws OrtException, IOException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + final OnnxModelRunner onnxModelRunner = givenOnnxModelRunner(); + + // when + final Map> impsBiddersFilterMap = target.filterBidders( + onnxModelRunner, throttlingMessages, threshold); + + // then + assertThat(impsBiddersFilterMap).isNotNull(); + assertThat(impsBiddersFilterMap.get("adUnit1").get("bidder1")).isTrue(); + assertThat(impsBiddersFilterMap.get("adUnit2").get("bidder2")).isFalse(); + assertThat(impsBiddersFilterMap.get("adUnit3").get("bidder3")).isFalse(); + } + + @Test + public void validateOnnxTensorShouldThrowPreBidExceptionWhenOnnxValueIsNotTensor() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results); + when(results.spliterator()).thenReturn(Arrays.asList(createInvalidOnnxItem()).spliterator()); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Expected OnnxTensor for 'probabilities', but found"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenOrtExceptionOccurs() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))) + .thenThrow(new OrtException("Exception during runModel")); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Exception during model inference"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenThrottlingMessagesIsEmpty() { + // given + final List throttlingMessages = Collections.emptyList(); + final Double threshold = 0.5; + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("throttlingMessages cannot be null or empty"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenTensorSizeMismatchOccurs() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results); + when(results.spliterator()).thenReturn(Arrays.asList(createOnnxItem()).spliterator()); + when(onnxTensor.getInfo()).thenReturn(tensorInfo); + when(tensorInfo.getShape()).thenReturn(new long[]{0}); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Mismatch between tensor size and throttlingMessages size"); + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } + + private List createThrottlingMessages() { + final ThrottlingMessage throttlingMessage1 = ThrottlingMessage.builder() + .browser("Chrome") + .bidder("bidder1") + .adUnitCode("adUnit1") + .country("US") + .hostname("localhost") + .device("PC") + .hourBucket("10") + .minuteQuadrant("1") + .build(); + + final ThrottlingMessage throttlingMessage2 = ThrottlingMessage.builder() + .browser("Firefox") + .bidder("bidder2") + .adUnitCode("adUnit2") + .country("FR") + .hostname("www.leparisien.fr") + .device("Mobile") + .hourBucket("11") + .minuteQuadrant("2") + .build(); + + final ThrottlingMessage throttlingMessage3 = ThrottlingMessage.builder() + .browser("Safari") + .bidder("bidder3") + .adUnitCode("adUnit3") + .country("FR") + .hostname("www.lesechos.fr") + .device("Tablet") + .hourBucket("12") + .minuteQuadrant("3") + .build(); + + return Arrays.asList(throttlingMessage1, throttlingMessage2, throttlingMessage3); + } + + private Map.Entry createOnnxItem() { + return new AbstractMap.SimpleEntry<>("probabilities", onnxTensor); + } + + private Map.Entry createInvalidOnnxItem() { + return new AbstractMap.SimpleEntry<>("probabilities", onnxValue); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java new file mode 100644 index 00000000000..1ac1bcc5cb1 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java @@ -0,0 +1,165 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.Country; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; + +import java.io.IOException; +import java.net.InetAddress; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsInferenceDataServiceTest { + + @Mock(strictness = LENIENT) + private DatabaseReaderFactory databaseReaderFactory; + + @Mock + private DatabaseReader databaseReader; + + @Mock + private Country country; + + private GreenbidsInferenceDataService target; + + @BeforeEach + public void setUp() { + when(databaseReaderFactory.getDatabaseReader()).thenReturn(databaseReader); + target = new GreenbidsInferenceDataService(databaseReaderFactory, TestBidRequestProvider.MAPPER); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldReturnValidThrottlingMessages() + throws IOException, GeoIp2Exception { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + + final CountryResponse countryResponse = mock(CountryResponse.class); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + when(databaseReader.country(any(InetAddress.class))).thenReturn(countryResponse); + when(countryResponse.getCountry()).thenReturn(country); + when(country.getName()).thenReturn("US"); + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + assertThat(throttlingMessages.getFirst().getBidder()).isEqualTo("rubicon"); + assertThat(throttlingMessages.get(1).getBidder()).isEqualTo("appnexus"); + assertThat(throttlingMessages.getLast().getBidder()).isEqualTo("pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo("US"); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldHandleMissingIp() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDeviceWithoutIp(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + + assertThat(throttlingMessages.getFirst().getBidder()).isEqualTo("rubicon"); + assertThat(throttlingMessages.get(1).getBidder()).isEqualTo("appnexus"); + assertThat(throttlingMessages.getLast().getBidder()).isEqualTo("pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo(StringUtils.EMPTY); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldThrowPreBidExceptionWhenGeoIpFails() + throws IOException, GeoIp2Exception { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + + when(databaseReader.country(any(InetAddress.class))).thenThrow(new GeoIp2Exception("GeoIP failure")); + + // when & then + assertThatThrownBy(() -> target.extractThrottlingMessagesFromBidRequest(bidRequest)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Failed to fetch country from geoLite DB"); + } + + private Device givenDeviceWithoutIp(UnaryOperator deviceCustomizer) { + final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36"; + return deviceCustomizer.apply(Device.builder().ua(userAgent)).build(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java new file mode 100644 index 00000000000..1bf0f5409e4 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java @@ -0,0 +1,126 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsInvocationServiceTest { + + private GreenbidsInvocationService target; + + @BeforeEach + public void setUp() { + target = new GreenbidsInvocationService(); + } + + @Test + public void createGreenbidsInvocationResultShouldReturnUpdateBidRequestWhenNotExploration() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + final Map> impsBiddersFilterMap = givenImpsBiddersFilterMap(); + final Partner partner = givenPartner(0.0); + + // when + final GreenbidsInvocationResult result = target.createGreenbidsInvocationResult( + partner, bidRequest, impsBiddersFilterMap); + + // then + final JsonNode updatedBidRequestExtPrebidBidders = result.getUpdatedBidRequest().getImp().getFirst().getExt() + .get("prebid").get("bidder"); + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.update); + assertThat(updatedBidRequestExtPrebidBidders.has("rubicon")).isTrue(); + assertThat(updatedBidRequestExtPrebidBidders.has("appnexus")).isFalse(); + assertThat(updatedBidRequestExtPrebidBidders.has("pubmatic")).isFalse(); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isFalse(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isTrue(); + assertThat(keptInAuction.get("appnexus")).isFalse(); + assertThat(keptInAuction.get("pubmatic")).isFalse(); + + } + + @Test + public void createGreenbidsInvocationResultShouldReturnNoActionWhenExploration() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + final Map> impsBiddersFilterMap = givenImpsBiddersFilterMap(); + final Partner partner = givenPartner(1.0); + + // when + final GreenbidsInvocationResult result = target.createGreenbidsInvocationResult( + partner, bidRequest, impsBiddersFilterMap); + + // then + final JsonNode updatedBidRequestExtPrebidBidders = result.getUpdatedBidRequest().getImp().getFirst().getExt() + .get("prebid").get("bidder"); + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.no_action); + assertThat(updatedBidRequestExtPrebidBidders.has("rubicon")).isTrue(); + assertThat(updatedBidRequestExtPrebidBidders.has("appnexus")).isTrue(); + assertThat(updatedBidRequestExtPrebidBidders.has("pubmatic")).isTrue(); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isTrue(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isTrue(); + assertThat(keptInAuction.get("appnexus")).isTrue(); + assertThat(keptInAuction.get("pubmatic")).isTrue(); + } + + private Map> givenImpsBiddersFilterMap() { + final Map biddersFitlerMap = new HashMap<>(); + biddersFitlerMap.put("rubicon", true); + biddersFitlerMap.put("appnexus", false); + biddersFitlerMap.put("pubmatic", false); + + final Map> impsBiddersFilterMap = new HashMap<>(); + impsBiddersFilterMap.put("adunitcodevalue", biddersFitlerMap); + + return impsBiddersFilterMap; + } + + private Partner givenPartner(Double explorationRate) { + return Partner.of("test-pbuid", 0.60, explorationRate); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java new file mode 100644 index 00000000000..b4839146a44 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java @@ -0,0 +1,59 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GreenbidsUserAgentTest { + + @Test + public void getDeviceShouldReturnPCWhenWindowsNTInUserAgent() { + // given + final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getDevice()).isEqualTo("PC"); + } + + @Test + public void getDeviceShouldReturnDeviceIPhoneWhenIOSInUserAgent() { + // given + final String userAgentString = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getDevice()).isEqualTo("iPhone"); + } + + @Test + public void getBrowserShouldReturnBrowserNameAndVersionWhenUserAgentIsPresent() { + // given + final String userAgentString = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getBrowser()).isEqualTo("Chrome 58"); + } + + @Test + public void getBrowserShouldReturnEmptyStringWhenBrowserIsNull() { + // given + final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getBrowser()).isEqualTo(StringUtils.EMPTY); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java new file mode 100644 index 00000000000..0c326f4249e --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java @@ -0,0 +1,191 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import com.google.cloud.storage.Blob; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.exception.PreBidException; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ModelCacheTest { + + private static final String GCS_BUCKET_NAME = "test_bucket"; + private static final String MODEL_CACHE_KEY_PREFIX = "onnxModelRunner_"; + private static final String PBUUID = "test-pbuid"; + private static final String ONNX_MODEL_PATH = "model.onnx"; + + @Mock + private Cache cache; + + @Mock(strictness = LENIENT) + private Storage storage; + + @Mock(strictness = LENIENT) + private Bucket bucket; + + @Mock(strictness = LENIENT) + private Blob blob; + + @Mock + private OnnxModelRunner onnxModelRunner; + + @Mock(strictness = LENIENT) + private OnnxModelRunnerFactory onnxModelRunnerFactory; + + @Mock + private ModelCache target; + + private Vertx vertx; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new ModelCache( + storage, GCS_BUCKET_NAME, cache, MODEL_CACHE_KEY_PREFIX, vertx, onnxModelRunnerFactory); + } + + @Test + public void getShouldReturnModelFromCacheWhenPresent() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(onnxModelRunner); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(onnxModelRunner); + verify(cache).getIfPresent(eq(cacheKey)); + } + + @Test + public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + final ModelCache spyModelCache = spy(target); + final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(mockFetchingState.compareAndSet(false, true)).thenReturn(false); + final Field isFetchingField = ModelCache.class.getDeclaredField("isFetching"); + isFetchingField.setAccessible(true); + isFetchingField.set(spyModelCache, mockFetchingState); + + // when + final Future result = spyModelCache.get(ONNX_MODEL_PATH, PBUUID); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo( + "ModelRunner fetching in progress. Skip current request"); + } + + @Test + public void getShouldFetchModelWhenNotInCache() throws OrtException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + final byte[] bytes = new byte[]{1, 2, 3}; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(onnxModelRunnerFactory.create(bytes)).thenReturn(onnxModelRunner); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result()).isEqualTo(onnxModelRunner); + verify(cache).put(eq(cacheKey), eq(onnxModelRunner)); + }); + } + + @Test + public void getShouldThrowExceptionWhenStorageFails() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for model"); + }); + } + + @Test + public void getShouldThrowExceptionWhenOnnxModelFails() throws OrtException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + final byte[] bytes = new byte[]{1, 2, 3}; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(onnxModelRunnerFactory.create(bytes)).thenThrow( + new OrtException("Failed to convert blob to ONNX model")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Failed to convert blob to ONNX model"); + }); + } + + @Test + public void getShouldThrowExceptionWhenBucketNotFound() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenThrow(new PreBidException("Bucket not found")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Bucket not found"); + }); + } + +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java new file mode 100644 index 00000000000..4a18b0a0fc0 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java @@ -0,0 +1,73 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Objects; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class OnnxModelRunnerTest { + + private OnnxModelRunner target; + + @BeforeEach + public void setUp() throws OrtException, IOException { + target = givenOnnxModelRunner(); + } + + @Test + public void runModelShouldReturnProbabilitiesWhenValidThrottlingInferenceRow() throws OrtException { + // given + final String[][] throttlingInferenceRow = {{ + "Chrome 59", "rubicon", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}}; + + // when + final OrtSession.Result actualResult = target.runModel(throttlingInferenceRow); + + // then + final float[][] probabilities = StreamSupport.stream(actualResult.spliterator(), false) + .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities")) + .map(Map.Entry::getValue) + .map(OnnxTensor.class::cast) + .map(tensor -> { + try { + return (float[][]) tensor.getValue(); + } catch (OrtException e) { + throw new RuntimeException(e); + } + }).findFirst().get(); + + assertThat(actualResult).isNotNull(); + assertThat(actualResult).hasSize(2); + assertThat(probabilities[0]).isNotEmpty(); + assertThat(probabilities[0][0]).isBetween(0.0f, 1.0f); + assertThat(probabilities[0][1]).isBetween(0.0f, 1.0f); + } + + @Test + public void runModelShouldThrowOrtExceptionWhenNonValidThrottlingInferenceRow() { + // given + final String[][] throttlingInferenceRowWithMissingColumn = {{ + "Chrome 59", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}}; + + // when & then + assertThatThrownBy(() -> target.runModel(throttlingInferenceRowWithMissingColumn)) + .isInstanceOf(OrtException.class); + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java new file mode 100644 index 00000000000..90a8d521f71 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java @@ -0,0 +1,198 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ThresholdCacheTest { + + private static final String GCS_BUCKET_NAME = "test_bucket"; + private static final String THRESHOLD_CACHE_KEY_PREFIX = "onnxModelRunner_"; + private static final String PBUUID = "test-pbuid"; + private static final String THRESHOLDS_PATH = "thresholds.json"; + + @Mock + private Cache cache; + + @Mock(strictness = LENIENT) + private Storage storage; + + @Mock(strictness = LENIENT) + private Bucket bucket; + + @Mock(strictness = LENIENT) + private Blob blob; + + @Mock + private ThrottlingThresholds throttlingThresholds; + + @Mock(strictness = LENIENT) + private ThrottlingThresholdsFactory throttlingThresholdsFactory; + + private Vertx vertx; + + private ThresholdCache target; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new ThresholdCache( + storage, + GCS_BUCKET_NAME, + TestBidRequestProvider.MAPPER, + cache, + THRESHOLD_CACHE_KEY_PREFIX, + vertx, + throttlingThresholdsFactory); + } + + @Test + public void getShouldReturnThresholdsFromCacheWhenPresent() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(throttlingThresholds); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(throttlingThresholds); + verify(cache).getIfPresent(eq(cacheKey)); + } + + @Test + public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + + final ThresholdCache spyThresholdCache = spy(target); + final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(mockFetchingState.compareAndSet(false, true)).thenReturn(false); + + final Field isFetchingField = ThresholdCache.class.getDeclaredField("isFetching"); + isFetchingField.setAccessible(true); + isFetchingField.set(spyThresholdCache, mockFetchingState); + + // when + final Future result = spyThresholdCache.get(THRESHOLDS_PATH, PBUUID); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo( + "ThrottlingThresholds fetching in progress. Skip current request"); + } + + @Test + public void getShouldFetchThresholdsWhenNotInCache() throws IOException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + final String jsonContent = "test_json_content"; + final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)) + .thenReturn(throttlingThresholds); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result()).isEqualTo(throttlingThresholds); + verify(cache).put(eq(cacheKey), eq(throttlingThresholds)); + }); + } + + @Test + public void getShouldThrowExceptionWhenStorageFails() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for threshold"); + }); + } + + @Test + public void getShouldThrowExceptionWhenLoadingJsonFails() throws IOException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + final String jsonContent = "test_json_content"; + final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8); + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)).thenThrow( + new IOException("Failed to load throttling thresholds json")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Failed to load throttling thresholds json"); + }); + } + + @Test + public void getShouldThrowExceptionWhenBucketNotFound() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenThrow(new PreBidException("Bucket not found")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Bucket not found"); + }); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java new file mode 100644 index 00000000000..11ca069e447 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java @@ -0,0 +1,93 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +public class TestBidRequestProvider { + + public static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + private TestBidRequestProvider() { } + + public static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + List imps, + Device device, + ExtRequest extRequest) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("request") + .imp(imps) + .site(givenSite(site -> site)) + .device(device) + .ext(extRequest)).build(); + } + + public static Site givenSite(UnaryOperator siteCustomizer) { + return siteCustomizer.apply(Site.builder().domain("www.leparisien.fr")).build(); + } + + public static ObjectNode givenImpExt() { + final ObjectNode bidderNode = MAPPER.createObjectNode(); + + final ObjectNode rubiconNode = MAPPER.createObjectNode(); + rubiconNode.put("accountId", 1001); + rubiconNode.put("siteId", 267318); + rubiconNode.put("zoneId", 1861698); + bidderNode.set("rubicon", rubiconNode); + + final ObjectNode appnexusNode = MAPPER.createObjectNode(); + appnexusNode.put("placementId", 123456); + bidderNode.set("appnexus", appnexusNode); + + final ObjectNode pubmaticNode = MAPPER.createObjectNode(); + pubmaticNode.put("publisherId", "156209"); + pubmaticNode.put("adSlot", "slot1@300x250"); + bidderNode.set("pubmatic", pubmaticNode); + + final ObjectNode prebidNode = MAPPER.createObjectNode(); + prebidNode.set("bidder", bidderNode); + + final ObjectNode extNode = MAPPER.createObjectNode(); + extNode.set("prebid", prebidNode); + extNode.set("tid", TextNode.valueOf("67eaab5f-27a6-4689-93f7-bd8f024576e3")); + + return extNode; + } + + public static Device givenDevice(UnaryOperator deviceCustomizer) { + final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36"; + return deviceCustomizer.apply(Device.builder().ua(userAgent).ip("151.101.194.216")).build(); + } + + public static Device givenDeviceWithoutUserAgent(UnaryOperator deviceCustomizer) { + return deviceCustomizer.apply(Device.builder().ip("151.101.194.216")).build(); + } + + public static Banner givenBanner() { + final Format format = Format.builder() + .w(320) + .h(50) + .build(); + + return Banner.builder() + .format(Collections.singletonList(format)) + .w(240) + .h(400) + .build(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..157b70b9474 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java @@ -0,0 +1,466 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1; + +import ai.onnxruntime.OrtException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDeviceWithoutUserAgent; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenSite; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsRealTimeDataProcessedAuctionRequestHookTest { + + @Mock + private Cache modelCacheWithExpiration; + + @Mock + private Cache thresholdsCacheWithExpiration; + + @Mock(strictness = LENIENT) + private DatabaseReaderFactory databaseReaderFactory; + + @Mock + private DatabaseReader dbReader; + + private GreenbidsRealTimeDataProcessedAuctionRequestHook target; + + @BeforeEach + public void setUp() throws IOException { + final Storage storage = StorageOptions.newBuilder() + .setProjectId("test_project").build().getService(); + final FilterService filterService = new FilterService(); + final OnnxModelRunnerFactory onnxModelRunnerFactory = new OnnxModelRunnerFactory(); + final ThrottlingThresholdsFactory throttlingThresholdsFactory = new ThrottlingThresholdsFactory(); + final ModelCache modelCache = new ModelCache( + storage, + "test_bucket", + modelCacheWithExpiration, + "onnxModelRunner_", + Vertx.vertx(), + onnxModelRunnerFactory); + final ThresholdCache thresholdCache = new ThresholdCache( + storage, + "test_bucket", + TestBidRequestProvider.MAPPER, + thresholdsCacheWithExpiration, + "throttlingThresholds_", + Vertx.vertx(), + throttlingThresholdsFactory); + final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds = new OnnxModelRunnerWithThresholds( + modelCache, + thresholdCache); + when(databaseReaderFactory.getDatabaseReader()).thenReturn(dbReader); + final GreenbidsInferenceDataService greenbidsInferenceDataService = new GreenbidsInferenceDataService( + databaseReaderFactory, + TestBidRequestProvider.MAPPER); + final GreenbidsInvocationService greenbidsInvocationService = new GreenbidsInvocationService(); + target = new GreenbidsRealTimeDataProcessedAuctionRequestHook( + TestBidRequestProvider.MAPPER, + filterService, + onnxModelRunnerWithThresholds, + greenbidsInferenceDataService, + greenbidsInvocationService); + } + + @Test + public void callShouldExitEarlyWhenPartnerNotActivatedInBidRequest() { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.analyticsTags()).isNull(); + } + + @Disabled("Broken until dbReader is mocked") + @Test + public void callShouldNotFilterBiddersAndReturnAnalyticsTagWhenExploration() throws OrtException, IOException { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Double explorationRate = 1.0; + final Device device = givenDevice(identity()); + final ExtRequest extRequest = givenExtRequest(explorationRate); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid")) + .thenReturn(givenOnnxModelRunner()); + when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid")) + .thenReturn(givenThrottlingThresholds()); + + final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(true, true); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + + // then + final ActivityImpl activity = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl resultImpl = (ResultImpl) activity.results().getFirst(); + final String fingerprint = resultImpl.values() + .get("adunitcodevalue") + .get("greenbids") + .get("fingerprint").asText(); + + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags()).usingRecursiveComparison() + .ignoringFields( + "activities.results" + + ".values._children" + + ".adunitcodevalue._children" + + ".greenbids._children.fingerprint") + .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult))); + assertThat(fingerprint).isNotNull(); + } + + @Disabled("Broken until dbReader is mocked") + @Test + public void callShouldFilterBiddersBasedOnModelWhenAnyFeatureNotAvailable() throws OrtException, IOException { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Double explorationRate = 0.0001; + final Device device = givenDeviceWithoutUserAgent(identity()); + final ExtRequest extRequest = givenExtRequest(explorationRate); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid")) + .thenReturn(givenOnnxModelRunner()); + when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid")) + .thenReturn(givenThrottlingThresholds()); + + final BidRequest expectedBidRequest = expectedUpdatedBidRequest(request -> request, explorationRate, device); + final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(false, false); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + final BidRequest resultBidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(bidRequest)) + .bidRequest(); + + // then + final ActivityImpl activity = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl resultImpl = (ResultImpl) activity.results().getFirst(); + final String fingerprint = resultImpl.values() + .get("adunitcodevalue") + .get("greenbids") + .get("fingerprint").asText(); + + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags()).usingRecursiveComparison() + .ignoringFields( + "activities.results" + + ".values._children" + + ".adunitcodevalue._children" + + ".greenbids._children.fingerprint") + .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult))); + assertThat(fingerprint).isNotNull(); + assertThat(resultBidRequest).usingRecursiveComparison().isEqualTo(expectedBidRequest); + } + + @Disabled("Broken until dbReader is mocked") + @Test + public void callShouldFilterBiddersBasedOnModelResults() throws OrtException, IOException { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Double explorationRate = 0.0001; + final Device device = givenDevice(identity()); + final ExtRequest extRequest = givenExtRequest(explorationRate); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid")) + .thenReturn(givenOnnxModelRunner()); + when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid")) + .thenReturn(givenThrottlingThresholds()); + + final BidRequest expectedBidRequest = expectedUpdatedBidRequest( + request -> request, explorationRate, device); + final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(false, false); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + final BidRequest resultBidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(bidRequest)) + .bidRequest(); + + // then + final ActivityImpl activityImpl = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl resultImpl = (ResultImpl) activityImpl.results().getFirst(); + final String fingerprint = resultImpl.values() + .get("adunitcodevalue") + .get("greenbids") + .get("fingerprint").asText(); + + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags()).usingRecursiveComparison() + .ignoringFields( + "activities.results" + + ".values._children" + + ".adunitcodevalue._children" + + ".greenbids._children.fingerprint") + .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult))); + assertThat(fingerprint).isNotNull(); + assertThat(resultBidRequest).usingRecursiveComparison() + .isEqualTo(expectedBidRequest); + } + + static ExtRequest givenExtRequest(Double explorationRate) { + final ObjectNode greenbidsNode = TestBidRequestProvider.MAPPER.createObjectNode(); + greenbidsNode.put("pbuid", "test-pbuid"); + greenbidsNode.put("targetTpr", 0.60); + greenbidsNode.put("explorationRate", explorationRate); + + final ObjectNode analyticsNode = TestBidRequestProvider.MAPPER.createObjectNode(); + analyticsNode.set("greenbids-rtd", greenbidsNode); + + return ExtRequest.of(ExtRequestPrebid + .builder() + .analytics(analyticsNode) + .build()); + } + + private AuctionContext givenAuctionContext( + BidRequest bidRequest, + UnaryOperator auctionContextCustomizer) { + + final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .httpRequest(HttpRequestContext.builder().build()) + .bidRequest(bidRequest); + + return auctionContextCustomizer.apply(auctionContextBuilder).build(); + } + + private AuctionInvocationContext givenAuctionInvocationContext(AuctionContext auctionContext) { + final AuctionInvocationContext invocationContext = mock(AuctionInvocationContext.class); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + return invocationContext; + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } + + private ThrottlingThresholds givenThrottlingThresholds() throws IOException { + final JsonNode thresholdsJsonNode = TestBidRequestProvider.MAPPER.readTree( + Files.newInputStream(Paths.get( + "src/test/resources/thresholds_pbuid=test-pbuid.json"))); + return TestBidRequestProvider.MAPPER + .treeToValue(thresholdsJsonNode, ThrottlingThresholds.class); + } + + private BidRequest expectedUpdatedBidRequest( + UnaryOperator bidRequestCustomizer, + Double explorationRate, + Device device) { + + final Banner banner = givenBanner(); + + final ObjectNode bidderNode = TestBidRequestProvider.MAPPER.createObjectNode(); + final ObjectNode prebidNode = TestBidRequestProvider.MAPPER.createObjectNode(); + prebidNode.set("bidder", bidderNode); + + final ObjectNode extNode = TestBidRequestProvider.MAPPER.createObjectNode(); + extNode.set("prebid", prebidNode); + extNode.set("tid", TextNode.valueOf("67eaab5f-27a6-4689-93f7-bd8f024576e3")); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(extNode) + .banner(banner) + .build(); + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("request") + .imp(List.of(imp)) + .site(givenSite(site -> site)) + .device(device) + .ext(givenExtRequest(explorationRate))).build(); + } + + private AnalyticsResult expectedAnalyticsResult(Boolean isExploration, Boolean isKeptInAuction) { + return AnalyticsResult.of( + "success", + Map.of("adunitcodevalue", expectedOrtb2ImpExtResult(isExploration, isKeptInAuction)), + null, + null); + } + + private Ortb2ImpExtResult expectedOrtb2ImpExtResult(Boolean isExploration, Boolean isKeptInAuction) { + return Ortb2ImpExtResult.of( + expectedExplorationResult(isExploration, isKeptInAuction), "67eaab5f-27a6-4689-93f7-bd8f024576e3"); + } + + private ExplorationResult expectedExplorationResult(Boolean isExploration, Boolean isKeptInAuction) { + final Map keptInAuction = Map.of( + "appnexus", isKeptInAuction, + "pubmatic", isKeptInAuction, + "rubicon", isKeptInAuction); + return ExplorationResult.of("60a7c66c-c542-48c6-a319-ea7b9f97947f", keptInAuction, isExploration); + } + + private Tags toAnalyticsTags(List analyticsResults) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + "greenbids-filter", + "success", + toResults(analyticsResults)))); + } + + private List toResults(List analyticsResults) { + return analyticsResults.stream() + .map(this::toResult) + .toList(); + } + + private Result toResult(AnalyticsResult analyticsResult) { + return ResultImpl.of( + analyticsResult.getStatus(), + toObjectNode(analyticsResult.getValues()), + AppliedToImpl.builder() + .bidders(Collections.singletonList(analyticsResult.getBidder())) + .impIds(Collections.singletonList(analyticsResult.getImpId())) + .build()); + } + + private ObjectNode toObjectNode(Map values) { + return values != null ? TestBidRequestProvider.MAPPER.valueToTree(values) : null; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx new file mode 100644 index 00000000000..f0acc8c66fe Binary files /dev/null and b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx differ diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json b/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json new file mode 100644 index 00000000000..462a6459297 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json @@ -0,0 +1,14 @@ +{ + "thresholds": [ + 0.4, + 0.224, + 0.018, + 0.018 + ], + "tpr": [ + 0.8, + 0.95, + 0.99, + 0.9999 + ] +} diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index f6d298e0947..8f76b6d0184 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java index 599be6e981c..47b3e3204c7 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java @@ -18,6 +18,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.spring.config.bidder.model.MediaType; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -51,7 +52,11 @@ public class AccountConfigReader { private static final String ALLOWED_APP_FOR_DEALS_FIELD = "allowed-app-for-deals"; private static final String BLOCKED_BANNER_TYPE_FIELD = "blocked-banner-type"; private static final String BLOCKED_BANNER_ATTR_FIELD = "blocked-banner-attr"; + private static final String BLOCKED_VIDEO_ATTR_FIELD = "blocked-video-attr"; + private static final String BLOCKED_AUDIO_ATTR_FIELD = "blocked-audio-attr"; private static final String ALLOWED_BANNER_ATTR_FOR_DEALS = "allowed-banner-attr-for-deals"; + private static final String ALLOWED_VIDEO_ATTR_FOR_DEALS = "allowed-video-attr-for-deals"; + private static final String ALLOWED_AUDIO_ATTR_FOR_DEALS = "allowed-audio-attr-for-deals"; private static final String ACTION_OVERRIDES_FIELD = "action-overrides"; private static final String OVERRIDE_FIELD = "override"; private static final String CONDITIONS_FIELD = "conditions"; @@ -99,9 +104,15 @@ public Result blockedAttributesFor(BidRequest bidRequest) { final Result> bapp = blockedAttribute(BAPP_FIELD, String.class, BLOCKED_APP_FIELD, requestMediaTypes); final Result>> btype = - blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, bidRequest); - final Result>> battr = - blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest); + blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, BANNER_MEDIA_TYPE, bidRequest); + final Result>> bannerBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, BANNER_MEDIA_TYPE, bidRequest); + final Result>> videoBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, VIDEO_MEDIA_TYPE, bidRequest); + final Result>> audioBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, AUDIO_MEDIA_TYPE, bidRequest); + final Result>>> battr = + mergeBlockedAttributes(bannerBattr, videoBattr, audioBattr); return Result.of( toBlockedAttributes(badv, bcat, cattaxComplement, bapp, btype, battr), @@ -133,22 +144,39 @@ public Result responseBlockingConfigFor(BidderBid bidder ALLOWED_APP_FOR_DEALS_FIELD, bidMediaTypes, dealid); - final Result> battr = blockingConfigForAttribute( + final Result> bannerBattr = blockingConfigForAttribute( BATTR_FIELD, Integer.class, ALLOWED_BANNER_ATTR_FOR_DEALS, bidMediaTypes, dealid); + final Result> videoBattr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_VIDEO_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + final Result> audioBattr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_AUDIO_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + final Map> battr = new HashMap<>(); + battr.put(MediaType.BANNER, bannerBattr.getValue()); + battr.put(MediaType.VIDEO, videoBattr.getValue()); + battr.put(MediaType.AUDIO, audioBattr.getValue()); final ResponseBlockingConfig response = ResponseBlockingConfig.builder() .badv(badv.getValue()) .bcat(bcat.getValue()) .cattax(cattax.getValue()) .bapp(bapp.getValue()) - .battr(battr.getValue()) + .battr(battr) .build(); - final List warnings = MergeUtils.mergeMessages(badv, bcat, cattax, bapp, battr); + final List warnings = MergeUtils.mergeMessages( + badv, bcat, cattax, bapp, bannerBattr, videoBattr, audioBattr); return Result.of(response, warnings); } @@ -198,19 +226,23 @@ private Integer blockedCattaxComplementFromConfig() { private Result>> blockedAttributesForImps(String attribute, Class attributeType, String fieldName, + String attributeMediaType, BidRequest bidRequest) { final Map> attributeValues = new HashMap<>(); final List> results = new ArrayList<>(); for (final Imp imp : bidRequest.getImp()) { - final Result> attributeForImp = blockedAttribute( - attribute, attributeType, fieldName, mediaTypesFrom(imp)); - - if (attributeForImp.hasValue()) { - attributeValues.put(imp.getId(), attributeForImp.getValue()); + final Set actualMediaTypes = mediaTypesFrom(imp); + if (actualMediaTypes.contains(attributeMediaType)) { + final Result> attributeForImp = blockedAttribute( + attribute, attributeType, fieldName, actualMediaTypes); + + if (attributeForImp.hasValue()) { + attributeValues.put(imp.getId(), attributeForImp.getValue()); + } + results.add(attributeForImp); } - results.add(attributeForImp); } return Result.of( @@ -218,6 +250,28 @@ private Result>> blockedAttributesForImps(String attribu MergeUtils.mergeMessages(results)); } + private static Result>>> mergeBlockedAttributes( + Result>> bannerBattr, + Result>> videoBattr, + Result>> audioBattr) { + + final Map>> battr = new HashMap<>(); + + if (bannerBattr.hasValue()) { + battr.put(MediaType.BANNER, bannerBattr.getValue()); + } + if (videoBattr.hasValue()) { + battr.put(MediaType.VIDEO, videoBattr.getValue()); + } + if (audioBattr.hasValue()) { + battr.put(MediaType.AUDIO, audioBattr.getValue()); + } + + return Result.of( + !battr.isEmpty() ? battr : null, + MergeUtils.mergeMessages(bannerBattr, videoBattr, audioBattr)); + } + private Result> blockingConfigForAttribute(String attribute, Class attributeType, String blockUnknownField, @@ -360,8 +414,8 @@ private Result toResult(List specificBidderResults, Set actualMediaTypes) { final JsonNode value = ObjectUtils.firstNonNull( - specificBidderResults.size() > 0 ? specificBidderResults.get(0) : null, - catchAllBidderResults.size() > 0 ? catchAllBidderResults.get(0) : null); + !specificBidderResults.isEmpty() ? specificBidderResults.getFirst() : null, + !catchAllBidderResults.isEmpty() ? catchAllBidderResults.getFirst() : null); final List warnings = debugEnabled && specificBidderResults.size() + catchAllBidderResults.size() > 1 ? Collections.singletonList( "More than one conditions matches request. Bidder: %s, request media types: %s" @@ -376,7 +430,7 @@ private static BlockedAttributes toBlockedAttributes(Result> badv, Result cattaxComplement, Result> bapp, Result>> btype, - Result>> battr) { + Result>>> battr) { return badv.hasValue() || bcat.hasValue() diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java index 28bdccdd136..5435544e2cb 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java @@ -5,6 +5,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; @@ -16,6 +18,8 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.ArrayList; import java.util.Collections; @@ -45,6 +49,7 @@ public class BidsBlocker { private final OrtbVersion ortbVersion; private final ObjectNode accountConfig; private final BlockedAttributes blockedAttributes; + private final BidRejectionTracker bidRejectionTracker; private final boolean debugEnabled; private BidsBlocker(List bids, @@ -52,6 +57,7 @@ private BidsBlocker(List bids, OrtbVersion ortbVersion, ObjectNode accountConfig, BlockedAttributes blockedAttributes, + BidRejectionTracker bidRejectionTracker, boolean debugEnabled) { this.bids = bids; @@ -59,6 +65,7 @@ private BidsBlocker(List bids, this.ortbVersion = ortbVersion; this.accountConfig = accountConfig; this.blockedAttributes = blockedAttributes; + this.bidRejectionTracker = bidRejectionTracker; this.debugEnabled = debugEnabled; } @@ -67,6 +74,7 @@ public static BidsBlocker create(List bids, OrtbVersion ortbVersion, ObjectNode accountConfig, BlockedAttributes blockedAttributes, + BidRejectionTracker bidRejectionTracker, boolean debugEnabled) { return new BidsBlocker( @@ -75,6 +83,7 @@ public static BidsBlocker create(List bids, Objects.requireNonNull(ortbVersion), accountConfig, blockedAttributes, + bidRejectionTracker, debugEnabled); } @@ -84,7 +93,6 @@ public ExecutionResult block() { try { final List> blockedBidResults = bids.stream() - .sequential() .map(bid -> isBlocked(bid, accountConfigReader)) .toList(); @@ -96,6 +104,11 @@ public ExecutionResult block() { final BlockedBids blockedBids = !blockedBidIndexes.isEmpty() ? BlockedBids.of(blockedBidIndexes) : null; final List warnings = MergeUtils.mergeMessages(blockedBidResults); + if (blockedBids != null) { + blockedBidIndexes.forEach(index -> + rejectBlockedBid(blockedBidResults.get(index).getValue(), bids.get(index))); + } + return ExecutionResult.builder() .value(blockedBids) .debugMessages(blockedBids != null ? debugMessages(blockedBidIndexes, blockedBidResults) : null) @@ -159,11 +172,30 @@ private AttributeCheckResult checkBapp(BidderBid bidderBid, ResponseBloc } private AttributeCheckResult checkBattr(BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { - + final MediaType mediaType = mapBidTypeToMediaType(bidderBid.getType()); return checkAttribute( bidderBid.getBid().getAttr(), - blockingConfig.getBattr(), - blockedAttributeValues(BlockedAttributes::getBattr, bidderBid.getBid().getImpid())); + blockingConfig.getBattr().get(mediaType), + blockedAttributeValues( + blockedAttributes -> extractBattrForMediaType(blockedAttributes, mediaType), + bidderBid.getBid().getImpid())); + } + + private static MediaType mapBidTypeToMediaType(BidType bidType) { + return switch (bidType) { + case banner -> MediaType.BANNER; + case video -> MediaType.VIDEO; + case audio -> MediaType.AUDIO; + case xNative -> MediaType.NATIVE; + case null -> null; + }; + } + + private static Map> extractBattrForMediaType(BlockedAttributes blockedAttributes, + MediaType mediaType) { + + final Map>> battr = blockedAttributes.getBattr(); + return battr != null ? battr.get(mediaType) : null; } private AttributeCheckResult checkAttribute(List attribute, @@ -256,6 +288,23 @@ private String debugEntryFor(int index, BlockingResult blockingResult) { blockingResult.getFailedChecks()); } + private void rejectBlockedBid(BlockingResult blockingResult, BidderBid blockedBid) { + if (blockingResult.getBattrCheckResult().isFailed() + || blockingResult.getBappCheckResult().isFailed() + || blockingResult.getBcatCheckResult().isFailed()) { + + bidRejectionTracker.rejectBid( + blockedBid, + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + } + + if (blockingResult.getBadvCheckResult().isFailed()) { + bidRejectionTracker.rejectBid( + blockedBid, + BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + } + } + private List toAnalyticsResults(List> blockedBidResults) { return blockedBidResults.stream() .map(Result::getValue) diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java index f83d8554a2c..78744e7c07f 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java @@ -1,11 +1,14 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; import java.util.Map; @@ -40,39 +43,99 @@ public BidRequest update(BidRequest bidRequest) { private List updateImps(List imps) { final Map> blockedBannerType = blockedAttributes.getBtype(); - final Map> blockedBannerAttr = blockedAttributes.getBattr(); + final Map>> blockedAttr = blockedAttributes.getBattr(); - if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedBannerAttr)) { + if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedAttr)) { return imps; } return imps.stream() - .map(imp -> updateImp(imp, blockedBannerType, blockedBannerAttr)) + .map(imp -> updateImp(imp, blockedBannerType, blockedAttr)) .toList(); } private Imp updateImp(Imp imp, Map> blockedBannerType, - Map> blockedBannerAttr) { + Map>> blockedAttr) { final String impId = imp.getId(); final List btypeForImp = blockedBannerType != null ? blockedBannerType.get(impId) : null; - final List battrForImp = blockedBannerAttr != null ? blockedBannerAttr.get(impId) : null; + final List bannerBattrForImp = extractBattr(blockedAttr, MediaType.BANNER, impId); + final List videoBattrForImp = extractBattr(blockedAttr, MediaType.VIDEO, impId); + final List audioBattrForImp = extractBattr(blockedAttr, MediaType.AUDIO, impId); + + if (CollectionUtils.isEmpty(btypeForImp) + && CollectionUtils.isEmpty(bannerBattrForImp) + && CollectionUtils.isEmpty(videoBattrForImp) + && CollectionUtils.isEmpty(audioBattrForImp)) { - if (CollectionUtils.isEmpty(btypeForImp) && CollectionUtils.isEmpty(battrForImp)) { return imp; } final Banner banner = imp.getBanner(); - final List existingBtype = banner != null ? banner.getBtype() : null; - final List existingBattr = banner != null ? banner.getBattr() : null; - final Banner.BannerBuilder bannerBuilder = banner != null ? banner.toBuilder() : Banner.builder(); + final Video video = imp.getVideo(); + final Audio audio = imp.getAudio(); return imp.toBuilder() - .banner(bannerBuilder - .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btypeForImp) - .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battrForImp) - .build()) + .banner(CollectionUtils.isNotEmpty(btypeForImp) || CollectionUtils.isNotEmpty(bannerBattrForImp) + ? updateBanner(banner, btypeForImp, bannerBattrForImp) + : banner) + .video(CollectionUtils.isNotEmpty(videoBattrForImp) + ? updateVideo(imp.getVideo(), videoBattrForImp) + : video) + .audio(CollectionUtils.isNotEmpty(audioBattrForImp) + ? updateAudio(imp.getAudio(), audioBattrForImp) + : audio) .build(); } + + private static List extractBattr(Map>> blockedAttr, + MediaType mediaType, + String impId) { + + final Map> impIdToBattr = blockedAttr != null ? blockedAttr.get(mediaType) : null; + return impIdToBattr != null ? impIdToBattr.get(impId) : null; + } + + private static Banner updateBanner(Banner banner, List btype, List battr) { + if (banner == null) { + return null; + } + + final List existingBtype = banner.getBtype(); + final List existingBattr = banner.getBattr(); + + return CollectionUtils.isEmpty(existingBtype) || CollectionUtils.isEmpty(existingBattr) + ? banner.toBuilder() + .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btype) + .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battr) + .build() + : banner; + } + + private static Video updateVideo(Video video, List battr) { + if (video == null) { + return null; + } + + final List existingBattr = video.getBattr(); + return CollectionUtils.isEmpty(existingBattr) + ? video.toBuilder() + .battr(battr) + .build() + : video; + } + + private static Audio updateAudio(Audio audio, List battr) { + if (audio == null) { + return null; + } + + final List existingBattr = audio.getBattr(); + return CollectionUtils.isEmpty(existingBattr) + ? audio.toBuilder() + .battr(battr) + .build() + : audio; + } } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java index d3d3049b57c..aad04ba8db6 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; import java.util.Map; @@ -20,5 +21,5 @@ public class BlockedAttributes { Map> btype; - Map> battr; + Map>> battr; } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java index c2108eb8a8f..8c34561079e 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java @@ -2,6 +2,9 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.spring.config.bidder.model.MediaType; + +import java.util.Map; @Builder @Value @@ -15,5 +18,5 @@ public class ResponseBlockingConfig { BidAttributeBlockingConfig bapp; - BidAttributeBlockingConfig battr; + Map> battr; } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java index 0bd01505596..6ef69c93140 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java @@ -5,13 +5,13 @@ import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.BlockedAttributesResolver; import org.prebid.server.hooks.modules.ortb2.blocking.core.RequestUpdater; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -42,7 +42,8 @@ public Future> call(BidderRequestPayload final BidRequest bidRequest = bidderRequestPayload.bidRequest(); final ModuleContext moduleContext = moduleContext(invocationContext) - .with(bidder, bidderSupportedOrtbVersion(bidder, aliases(bidRequest))); + .with(bidder, bidderSupportedOrtbVersion( + bidder, aliases(invocationContext.auctionContext().getBidRequest()))); final ExecutionResult blockedAttributesResult = BlockedAttributesResolver .create( diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java index 7ba82dd5b09..720823f4513 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java @@ -6,18 +6,18 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.versionconverter.OrtbVersion; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.BidsBlocker; import org.prebid.server.hooks.modules.ortb2.blocking.core.ResponseUpdater; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -59,6 +59,7 @@ public Future> call(BidderResponsePayloa ObjectUtils.defaultIfNull(moduleContext.ortbVersionOf(bidder), OrtbVersion.ORTB_2_5), invocationContext.accountConfig(), moduleContext.blockedAttributesFor(bidder), + invocationContext.auctionContext().getBidRejectionTrackers().get(bidder), invocationContext.debugEnabled()) .block(); diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java deleted file mode 100644 index bd394217b21..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import com.iab.openrtb.request.BidRequest; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class BidderRequestPayloadImpl implements BidderRequestPayload { - - BidRequest bidRequest; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java deleted file mode 100644 index 72d678c89a5..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class BidderResponsePayloadImpl implements BidderResponsePayload { - - List bids; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java deleted file mode 100644 index 48be15fdf37..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - ModuleContext moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java index 456ac49939a..e20bf2c3dac 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java @@ -30,6 +30,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.HashMap; import java.util.HashSet; @@ -399,7 +400,7 @@ public void blockedAttributesForShouldReturnErrorWhenBlockedBannerTypeIsNotInteg final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + assertThatThrownBy(() -> reader.blockedAttributesFor(request(imp -> imp.banner(Banner.builder().build())))) .isInstanceOf(InvalidAccountConfigurationException.class) .hasMessage("blocked-banner-type field in account configuration has unexpected type. " + "Expected class java.lang.Integer"); @@ -699,17 +700,23 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri .btype(Attribute.btypeBuilder() .actionOverrides(AttributeActionOverrides.blocked(asList( ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("video")), + Conditions.of(singletonList("bidder1"), singletonList("banner")), singletonList(1)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("video")), + Conditions.of(singletonList("bidder1"), singletonList("banner")), singletonList(2)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("banner")), + Conditions.of(singletonList("bidder1"), singletonList("video")), singletonList(3)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("banner")), - singletonList(4))))) + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList(4)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("audio")), + singletonList(5)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("audio")), + singletonList(6))))) .build()) .build())); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -717,24 +724,20 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri // when and then final Map> expectedBtype = new HashMap<>(); expectedBtype.put("impId1", singletonList(1)); - expectedBtype.put("impId2", singletonList(3)); assertThat(reader .blockedAttributesFor(BidRequest.builder() .imp(asList( - Imp.builder().id("impId1").video(Video.builder().build()).build(), - Imp.builder().id("impId2").banner(Banner.builder().build()).build())) + Imp.builder().id("impId1").banner(Banner.builder().build()).build(), + Imp.builder().id("impId2").video(Video.builder().build()).build())) .build())) .isEqualTo(Result.of( BlockedAttributes.builder().btype(expectedBtype).build(), - asList( - "More than one conditions matches request. Bidder: bidder1, " + - "request media types: [video]", - "More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner]"))); + List.of("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner]"))); } @Test - public void blockedAttributesForShouldReturnResultWithAllAttributes() { + public void blockedAttributesForShouldReturnResultWithAllAttributesForBanner() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -766,7 +769,7 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { Conditions.of(singletonList("bidder1"), null), singletonList(3))))) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .blocked(asList(1, 2)) .actionOverrides(AttributeActionOverrides.blocked(singletonList( ArrayOverride.of( @@ -777,13 +780,119 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( - Result.withValue(BlockedAttributes.builder() + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").banner(Banner.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() .badv(singletonList("domain3.com")) .bcat(singletonList("cat3")) .bapp(singletonList("app3")) .btype(singletonMap("impId1", singletonList(3))) - .battr(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", singletonList(3)))) + .build())); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributesForVideo() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.videoBattrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").video(Video.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", singletonList(3)))) + .build())); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributesForAudio() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.audioBattrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").audio(Audio.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", singletonList(3)))) .build())); } @@ -1143,7 +1252,163 @@ public void responseBlockingConfigForShouldReturnResultWithMergedDealExceptionsW } @Test - public void responseBlockingConfigForShouldReturnAllAttributes() { + public void responseBlockingConfigForShouldReturnAllAttributesForBanner() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.bannerBattrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, Set.of("domain1.com", "domain2.com", "domain3.com"))) + .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) + .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) + .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, emptySet()))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributesForVideo() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.videoBattrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, Set.of("domain1.com", "domain2.com", "domain3.com"))) + .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) + .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) + .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, emptySet()))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributesForAudio() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -1188,7 +1453,7 @@ public void responseBlockingConfigForShouldReturnAllAttributes() { DealsConditions.of(singletonList("dealid1")), singletonList("app3"))))) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.audioBattrBuilder() .enforceBlocks(true) .allowedForDeals(asList(1, 2)) .actionOverrides(AttributeActionOverrides.response( @@ -1211,7 +1476,10 @@ public void responseBlockingConfigForShouldReturnAllAttributes() { .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) - .battr(BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)))) .build()); assertThat(result.getMessages()).isNull(); }); @@ -1240,10 +1508,15 @@ public void responseBlockingConfigForShouldReturnCattaxConfigDependsOnBcatConfig final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then + final Map> expectedBattr = new HashMap<>(); + expectedBattr.put(MediaType.BANNER, null); + expectedBattr.put(MediaType.VIDEO, null); + expectedBattr.put(MediaType.AUDIO, null); assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() .bcat(BidAttributeBlockingConfig.of(true, false, Set.of("cat1", "cat2", "cat3"))) .cattax(BidAttributeBlockingConfig.of(true, true, emptySet())) + .battr(expectedBattr) .build()); assertThat(result.getMessages()).isNull(); }); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java index 0e96c78eb3a..b595b9c5fdb 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java @@ -6,6 +6,11 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.response.Bid; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; @@ -16,6 +21,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.HashMap; import java.util.List; @@ -29,7 +35,12 @@ import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +@ExtendWith(MockitoExtension.class) public class BidsBlockerTest { private static final ObjectMapper mapper = new ObjectMapper() @@ -38,14 +49,18 @@ public class BidsBlockerTest { private static final OrtbVersion ORTB_VERSION = OrtbVersion.ORTB_2_5; + @Mock + private BidRejectionTracker bidRejectionTracker; + @Test public void shouldReturnEmptyResultWhenNoBlockingResponseConfig() { // given final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, null, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, null, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -55,12 +70,13 @@ public void shouldReturnEmptyResultWithErrorWhenInvalidAccountConfig() { .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.builder() .errors(singletonList("attributes field in account configuration is not an object")) .build()); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -70,10 +86,11 @@ public void shouldReturnEmptyResultWithoutErrorWhenInvalidAccountConfigAndDebugD .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.empty()); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -88,10 +105,11 @@ public void shouldReturnEmptyResultWhenBidWithoutAdomainAndBlockUnknownFalse() { // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -106,10 +124,11 @@ public void shouldReturnEmptyResultWhenBidWithoutAdomainAndEnforceBlocksFalseAnd // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -124,7 +143,7 @@ public void shouldReturnResultWithBidWhenBidWithoutAdomainAndBlockUnknownTrue() // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); @@ -142,10 +161,11 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndEnforceBlocksFals // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -160,10 +180,11 @@ public void shouldReturnEmptyResultWhenBidWithNotBlockedAdomain() { // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain2.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -176,12 +197,13 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndEnforceBlocksTr .build())); // when - final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidderBid bid = bid(bidBuilder -> bidBuilder.adomain(singletonList("domain1.com"))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, false); + final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); } @Test @@ -195,17 +217,18 @@ public void shouldReturnEmptyResultWhenBidWithAdomainAndNoBlockedAttributes() { // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); @@ -215,12 +238,59 @@ public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() .impid("impId2") .attr(singletonList(1)))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() - .battr(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedVideoAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.videoBattrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedAudioAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.audioBattrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -234,12 +304,13 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndInDealsExceptions .build())); // when - final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidderBid bid = bid(bidBuilder -> bidBuilder.adomain(singletonList("domain1.com"))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -253,12 +324,13 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndNotInDealsExcep .build())); // when - final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidderBid bid = bid(bidBuilder -> bidBuilder.adomain(singletonList("domain1.com"))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, false); + final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); } @Test @@ -272,8 +344,8 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { .build())); // when - final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidderBid bid = bid(); + final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -281,6 +353,7 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { assertThat(result.getDebugMessages()).containsOnly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [bcat]"); }); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); } @Test @@ -294,11 +367,12 @@ public void shouldReturnResultWithBidWithoutDebugMessageWhenBidIsBlockedAndDebug .build())); // when - final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidderBid bid = bid(); + final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); } @Test @@ -314,34 +388,35 @@ public void shouldReturnResultWithAnalyticsResults() { .bapp(Attribute.bappBuilder() .enforceBlocks(true) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); + final BidderBid bid1 = bid(bid -> bid + .impid("impId1") + .adomain(asList("domain2.com", "domain3.com", "domain4.com")) + .bundle("app2")); + final BidderBid bid2 = bid(bid -> bid + .impid("impId2") + .cat(asList("cat2", "cat3", "cat4")) + .attr(asList(2, 3, 4))); + final BidderBid bid3 = bid(bid -> bid + .impid("impId1") + .adomain(singletonList("domain5.com")) + .cat(singletonList("cat5")) + .bundle("app5") + .attr(singletonList(5))); + // when - final List bids = asList( - bid(bid -> bid - .impid("impId1") - .adomain(asList("domain2.com", "domain3.com", "domain4.com")) - .bundle("app2")), - bid(bid -> bid - .impid("impId2") - .cat(asList("cat2", "cat3", "cat4")) - .attr(asList(2, 3, 4))), - bid(bid -> bid - .impid("impId1") - .adomain(singletonList("domain5.com")) - .cat(singletonList("cat5")) - .bundle("app5") - .attr(singletonList(5)))); + final List bids = asList(bid1, bid2, bid3); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() .badv(asList("domain1.com", "domain2.com", "domain3.com")) .bcat(asList("cat1", "cat2", "cat3")) .bapp(asList("app1", "app2", "app3")) - .battr(singletonMap("impId2", asList(1, 2, 3))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId2", asList(1, 2, 3)))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -361,6 +436,11 @@ public void shouldReturnResultWithAnalyticsResults() { AnalyticsResult.of("success-blocked", analyticsResultValues2, "bidder1", "impId2"), AnalyticsResult.of("success-allow", null, "bidder1", "impId1")); }); + + verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid2, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verifyNoMoreInteractions(bidRejectionTracker); } @Test @@ -381,37 +461,44 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .enforceBlocks(true) .allowedForDeals(singletonList("app2")) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .allowedForDeals(singletonList(2)) .build()) .build())); // when - final List bids = asList( - bid(bid -> bid.adomain(singletonList("domain1.com"))), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1"))), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2"))), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app1")), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app2")), - bid(bid -> bid - .adomain(singletonList("domain2.com")) - .cat(singletonList("cat2")) - .bundle("app2") - .attr(singletonList(1))), - bid(bid -> bid - .adomain(singletonList("domain2.com")) - .cat(singletonList("cat2")) - .bundle("app2") - .attr(singletonList(2))), - bid()); + final BidderBid bid1 = bid(bid -> bid.adomain(singletonList("domain1.com"))); + final BidderBid bid2 = bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1"))); + final BidderBid bid3 = bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2"))); + final BidderBid bid4 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app1")); + final BidderBid bid5 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2")); + final BidderBid bid6 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2") + .attr(singletonList(1))); + final BidderBid bid7 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2") + .attr(singletonList(2))); + final BidderBid bid8 = bid(); + + final List bids = asList(bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() .badv(asList("domain1.com", "domain2.com")) .bcat(asList("cat1", "cat2")) .bapp(asList("app1", "app2")) - .battr(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -423,6 +510,15 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { "Bid 5 from bidder bidder1 has been rejected, failed checks: [battr]", "Bid 7 from bidder bidder1 has been rejected, failed checks: [badv, bcat]"); }); + + verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verify(bidRejectionTracker).rejectBid(bid2, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid4, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid6, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid8, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).rejectBid(bid8, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verifyNoMoreInteractions(bidRejectionTracker); } @Test @@ -443,12 +539,14 @@ public void shouldReturnEmptyResultForCattaxIfBidderSupportsLowerThan26() { bid(bid -> bid.cattax(3)), bid()); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()) .extracting(ExecutionResult::getValue) .isNull(); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -465,12 +563,14 @@ public void shouldPassBidIfCattaxIsNull() { final List bids = singletonList(bid()); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()) .extracting(ExecutionResult::getValue) .isNull(); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -489,7 +589,7 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { bid(bid -> bid.cattax(2))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().cattaxComplement(2).build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -497,6 +597,8 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { assertThat(result.getDebugMessages()).containsExactly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [cattax]"); }); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -515,7 +617,7 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { bid(bid -> bid.cattax(2))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -523,6 +625,8 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { assertThat(result.getDebugMessages()).containsExactly( "Bid 1 from bidder bidder1 has been rejected, failed checks: [cattax]"); }); + + verifyNoInteractions(bidRejectionTracker); } private static BidderBid bid() { diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java index 39b5807ab12..1f03f82edb1 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java @@ -1,12 +1,16 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; +import java.util.Map; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -138,10 +142,12 @@ public void shouldReplaceImpBtypeWhenAbsent() { } @Test - public void shouldReplaceImpBattrWhenAbsent() { + public void shouldReplaceImpBannerBattrWhenAbsent() { // given final RequestUpdater updater = RequestUpdater.create( - BlockedAttributes.builder().battr(singletonMap("impId1", asList(1, 2))).build()); + BlockedAttributes.builder() + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) + .build()); final BidRequest request = BidRequest.builder() .imp(singletonList(Imp.builder() .id("impId1") @@ -160,6 +166,56 @@ public void shouldReplaceImpBattrWhenAbsent() { .build()); } + @Test + public void shouldReplaceImpVideoBattrWhenAbsent() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", asList(1, 2)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldReplaceImpAudioBattrWhenAbsent() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", asList(1, 2)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .audio(Audio.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + @Test public void shouldNotChangeImpsWhenNoBlockedBannerTypeAndBlockedBannerAttr() { // given @@ -180,7 +236,43 @@ public void shouldNotChangeImpWhenNoBlockedBannerTypeAndBlockedBannerAttrForImp( final RequestUpdater updater = RequestUpdater.create( BlockedAttributes.builder() .btype(singletonMap("impId2", singletonList(1))) - .battr(singletonMap("impId2", singletonList(1))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId2", singletonList(1)))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedVideoAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId2", singletonList(1)))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedAudioAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId2", singletonList(1)))) .build()); final Imp imp = Imp.builder().build(); final BidRequest request = BidRequest.builder() @@ -198,7 +290,7 @@ public void shouldKeepImpBtypeWhenNoBlockedBannerTypeAndPresentBlockedBannerAttr // given final RequestUpdater updater = RequestUpdater.create( BlockedAttributes.builder() - .battr(singletonMap("impId1", singletonList(1))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", singletonList(1)))) .build()); final Imp imp = Imp.builder() .id("impId1") @@ -256,10 +348,130 @@ public void shouldUpdateAllAttributes() { .bcat(asList("cat1", "cat2")) .bapp(asList("app1", "app2")) .btype(singletonMap("impId1", asList(1, 2))) - .battr(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .battr(asList(1, 2)) + .build()) + .video(Video.builder().battr(asList(3, 4)).build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingBanner() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().battr(asList(3, 4)).build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingVideo() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .battr(asList(1, 2)) + .build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingAudio() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) .build()); final BidRequest request = BidRequest.builder() - .imp(singletonList(Imp.builder().id("impId1").build())) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .build())) .build(); // when and then @@ -273,6 +485,7 @@ public void shouldUpdateAllAttributes() { .btype(asList(1, 2)) .battr(asList(1, 2)) .build()) + .video(Video.builder().battr(asList(3, 4)).build()) .build())) .build()); } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java index 6e7d3257a33..e60354670e9 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java @@ -63,8 +63,18 @@ public static AttributeBuilder btypeBuilder() { .field("banner-type"); } - public static AttributeBuilder battrBuilder() { + public static AttributeBuilder bannerBattrBuilder() { return Attribute.builder() .field("banner-attr"); } + + public static AttributeBuilder videoBattrBuilder() { + return Attribute.builder() + .field("video-attr"); + } + + public static AttributeBuilder audioBattrBuilder() { + return Attribute.builder() + .field("audio-attr"); + } } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index 60dd8ed5867..5c1b7303831 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -14,9 +14,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; @@ -26,8 +29,6 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -35,11 +36,14 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.spring.config.bidder.model.Ortb; +import java.util.Map; + import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @ExtendWith(MockitoExtension.class) @@ -49,9 +53,12 @@ public class Ortb2BlockingBidderRequestHookTest { .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private BidderCatalog bidderCatalog; + @Mock(strictness = Mock.Strictness.LENIENT) + private BidRejectionTracker bidRejectionTracker; + private Ortb2BlockingBidderRequestHook hook; @BeforeEach @@ -64,10 +71,21 @@ public void setUp() { @Test public void shouldReturnResultWithNoActionWhenNoBlockingAttributes() { + // given + given(bidderCatalog.bidderInfoByName(anyString())) + .willReturn(bidderInfo(OrtbVersion.ORTB_2_6)); + given(bidderCatalog.bidderInfoByName(eq("bidder1Base"))) + .willReturn(bidderInfo(OrtbVersion.ORTB_2_5)); + // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", null, true)); + BidderInvocationContextImpl.of( + "bidder1", + Map.of("bidder1", "bidder1Base"), + bidRejectionTracker, + null, + true)); // then assertThat(result.succeeded()).isTrue(); @@ -87,7 +105,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -108,7 +126,7 @@ public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndD // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -134,7 +152,7 @@ public void shouldReturnResultWithModuleContextAndPayloadUpdate() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -186,7 +204,7 @@ public void shouldReturnResultWithUpdateActionAndWarning() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -223,7 +241,7 @@ public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -254,6 +272,7 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) { null, null, 0, + null, false, false, null, diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java index 1273024247a..351bfbb9d33 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java @@ -7,7 +7,18 @@ import com.iab.openrtb.response.Bid; import io.vertx.core.Future; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; @@ -17,12 +28,6 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -31,6 +36,7 @@ import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.response.BidType; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -39,6 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +@ExtendWith(MockitoExtension.class) public class Ortb2BlockingRawBidderResponseHookTest { private static final ObjectMapper mapper = new ObjectMapper() @@ -48,12 +55,15 @@ public class Ortb2BlockingRawBidderResponseHookTest { private final Ortb2BlockingRawBidderResponseHook hook = new Ortb2BlockingRawBidderResponseHook( ObjectMapperProvider.mapper()); + @Mock + private BidRejectionTracker bidRejectionTracker; + @Test public void shouldReturnResultWithNoActionWhenNoBidsBlocked() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", null, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, null, true)); // then assertThat(result.succeeded()).isTrue(); @@ -83,7 +93,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -104,7 +114,7 @@ public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndD // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -134,6 +144,9 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { BidderInvocationContextImpl.builder() .bidder("bidder1") .accountConfig(accountConfig) + .auctionContext(AuctionContext.builder() + .bidRejectionTrackers(Map.of("bidder1", bidRejectionTracker)) + .build()) .moduleContext(ModuleContext.create().with( "bidder1", BlockedAttributes.builder().badv(singletonList("domain2.com")).build())) .debugEnabled(true) @@ -211,7 +224,7 @@ public void shouldReturnResultWithUpdateActionAndWarning() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -245,7 +258,7 @@ public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -271,7 +284,7 @@ public void shouldReturnResultWithUpdateActionAndDebugMessage() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -297,7 +310,7 @@ public void shouldReturnResultWithUpdateActionAndNoDebugMessageWhenDebugDisabled // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java index 99d19db08da..d39d0a4ca5f 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java @@ -1,13 +1,19 @@ package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; import lombok.Builder; import lombok.Value; import lombok.experimental.Accessors; import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.execution.Timeout; +import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.model.Endpoint; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Map; @Accessors(fluent = true) @Builder @@ -28,9 +34,38 @@ public class BidderInvocationContextImpl implements BidderInvocationContext { String bidder; - public static BidderInvocationContext of(String bidder, ObjectNode accountConfig, boolean debugEnabled) { + public static BidderInvocationContext of(String bidder, + BidRejectionTracker bidRejectionTracker, + ObjectNode accountConfig, + boolean debugEnabled) { + + return BidderInvocationContextImpl.builder() + .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) + .build()) + .accountConfig(accountConfig) + .debugEnabled(debugEnabled) + .build(); + } + + public static BidderInvocationContext of(String bidder, + Map aliases, + BidRejectionTracker bidRejectionTracker, + ObjectNode accountConfig, + boolean debugEnabled) { + return BidderInvocationContextImpl.builder() .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(aliases) + .build())) + .build()) + .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) + .build()) .accountConfig(accountConfig) .debugEnabled(debugEnabled) .build(); diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml new file mode 100644 index 00000000000..55425a647c2 --- /dev/null +++ b/extra/modules/pb-request-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.18.0-SNAPSHOT + + + pb-request-correction + + pb-request-correction + Request correction module + diff --git a/extra/modules/pb-request-correction/src/lombok.config b/extra/modules/pb-request-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-request-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java new file mode 100644 index 00000000000..3daa937c37c --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class RequestCorrectionProvider { + + private final List correctionProducers; + + public RequestCorrectionProvider(List correctionProducers) { + this.correctionProducers = Objects.requireNonNull(correctionProducers); + } + + public List corrections(Config config, BidRequest bidRequest) { + return correctionProducers.stream() + .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) + .map(correctionProducer -> correctionProducer.produce(config)) + .toList(); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java new file mode 100644 index 00000000000..44cac23337e --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class Config { + + boolean enabled; + + @JsonAlias("pbsdkAndroidInstlRemove") + @JsonProperty("pbsdk-android-instl-remove") + boolean interstitialCorrectionEnabled; + + @JsonAlias("pbsdkUaCleanup") + @JsonProperty("pbsdk-ua-cleanup") + boolean userAgentCorrectionEnabled; +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java new file mode 100644 index 00000000000..2cfda5fce68 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; + +public interface Correction { + + BidRequest apply(BidRequest bidRequest); +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..a92132656d5 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(Config config); +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java new file mode 100644 index 00000000000..75d86c511fb --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; + +public class InterstitialCorrection implements Correction { + + @Override + public BidRequest apply(BidRequest bidRequest) { + return bidRequest.toBuilder() + .imp(bidRequest.getImp().stream() + .map(InterstitialCorrection::removeInterstitial) + .toList()) + .build(); + } + + private static Imp removeInterstitial(Imp imp) { + final Integer interstitial = imp.getInstl(); + return interstitial != null && interstitial == 1 + ? imp.toBuilder().instl(null).build() + : imp; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java new file mode 100644 index 00000000000..c9bd1995867 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java @@ -0,0 +1,80 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import java.util.List; +import java.util.Optional; + +public class InterstitialCorrectionProducer implements CorrectionProducer { + + private static final InterstitialCorrection CORRECTION_INSTANCE = new InterstitialCorrection(); + + private static final String PREBID_MOBILE = "prebid-mobile"; + private static final String ANDROID = "android"; + + private static final int MAX_VERSION_MAJOR = 2; + private static final int MAX_VERSION_MINOR = 2; + private static final int MAX_VERSION_PATCH = 3; + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final App app = bidRequest.getApp(); + return config.isInterstitialCorrectionEnabled() + && hasInterstitialToRemove(bidRequest.getImp()) + && isPrebidMobile(app) + && isAndroid(app) + && isApplicableVersion(app); + } + + private static boolean hasInterstitialToRemove(List imps) { + for (Imp imp : imps) { + final Integer interstitial = imp.getInstl(); + if (interstitial != null && interstitial == 1) { + return true; + } + } + + return false; + } + + private static boolean isPrebidMobile(App app) { + final String source = Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getSource) + .orElse(null); + + return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE); + } + + private static boolean isAndroid(App app) { + return StringUtils.containsIgnoreCase(app.getBundle(), ANDROID); + } + + private static boolean isApplicableVersion(App app) { + return Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getVersion) + .map(InterstitialCorrectionProducer::checkVersion) + .orElse(false); + } + + private static boolean checkVersion(String version) { + return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH); + } + + @Override + public Correction produce(Config config) { + return CORRECTION_INSTANCE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java new file mode 100644 index 00000000000..f1b6b40eacc --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; + +import java.util.regex.Pattern; + +public class UserAgentCorrection implements Correction { + + private static final Pattern USER_AGENT_PATTERN = Pattern.compile("PrebidMobile/[0-9][^ ]*"); + + @Override + public BidRequest apply(BidRequest bidRequest) { + return bidRequest.toBuilder() + .device(correctDevice(bidRequest.getDevice())) + .build(); + } + + private static Device correctDevice(Device device) { + return device.toBuilder() + .ua(USER_AGENT_PATTERN.matcher(device.getUa()).replaceAll("")) + .build(); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java new file mode 100644 index 00000000000..f4c8d4f76dd --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UserAgentCorrectionProducer implements CorrectionProducer { + + private static final UserAgentCorrection CORRECTION_INSTANCE = new UserAgentCorrection(); + + private static final String PREBID_MOBILE = "prebid-mobile"; + private static final Pattern USER_AGENT_PATTERN = Pattern.compile(".*PrebidMobile/[0-9]+[^ ]*.*"); + + + private static final int MAX_VERSION_MAJOR = 2; + private static final int MAX_VERSION_MINOR = 1; + private static final int MAX_VERSION_PATCH = 6; + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final App app = bidRequest.getApp(); + return config.isUserAgentCorrectionEnabled() + && isPrebidMobile(app) + && isApplicableVersion(app) + && isApplicableDevice(bidRequest.getDevice()); + } + + private static boolean isPrebidMobile(App app) { + final String source = Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getSource) + .orElse(null); + + return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE); + } + + private static boolean isApplicableVersion(App app) { + return Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getVersion) + .map(UserAgentCorrectionProducer::checkVersion) + .orElse(false); + } + + private static boolean checkVersion(String version) { + return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH); + } + + private static boolean isApplicableDevice(Device device) { + return Optional.ofNullable(device) + .map(Device::getUa) + .filter(StringUtils::isNotEmpty) + .map(USER_AGENT_PATTERN::matcher) + .map(Matcher::matches) + .orElse(false); + } + + + @Override + public Correction produce(Config config) { + return CORRECTION_INSTANCE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java new file mode 100644 index 00000000000..2e84f01183e --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.util; + +public class VersionUtil { + + public static boolean isVersionLessThan(String versionAsString, int major, int minor, int patch) { + return compareVersion(versionAsString, major, minor, patch) < 0; + } + + private static int compareVersion(String versionAsString, int major, int minor, int patch) { + final String[] version = versionAsString.split("\\."); + + final int parsedMajor = getAtAsIntOrDefault(version, 0, -1); + final int parsedMinor = getAtAsIntOrDefault(version, 1, 0); + final int parsedPatch = getAtAsIntOrDefault(version, 2, 0); + + int diff = parsedMajor >= 0 ? parsedMajor - major : 1; + diff = diff == 0 ? parsedMinor - minor : diff; + diff = diff == 0 ? parsedPatch - patch : diff; + + return diff; + } + + private static int getAtAsIntOrDefault(String[] array, int index, int defaultValue) { + return array.length > index ? intOrDefault(array[index], defaultValue) : defaultValue; + } + + private static int intOrDefault(String intAsString, int defaultValue) { + try { + final int parsed = Integer.parseInt(intAsString); + return parsed >= 0 ? parsed : defaultValue; + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..ecbd725e42d --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.request.correction.spring.config; + +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial.InterstitialCorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent.UserAgentCorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.v1.RequestCorrectionModule; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "hooks." + RequestCorrectionModule.CODE, name = "enabled", havingValue = "true") +public class RequestCorrectionModuleConfiguration { + + @Bean + InterstitialCorrectionProducer interstitialCorrectionProducer() { + return new InterstitialCorrectionProducer(); + } + + @Bean + UserAgentCorrectionProducer userAgentCorrectionProducer() { + return new UserAgentCorrectionProducer(); + } + + @Bean + RequestCorrectionProvider requestCorrectionProvider(List correctionProducers) { + return new RequestCorrectionProvider(correctionProducers); + } + + @Bean + RequestCorrectionModule requestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider) { + return new RequestCorrectionModule(requestCorrectionProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java new file mode 100644 index 00000000000..10d20a3b823 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Collections; + +public class RequestCorrectionModule implements Module { + + public static final String CODE = "pb-request-correction"; + + private final Collection> hooks; + + public RequestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java new file mode 100644 index 00000000000..b9142c93d26 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java @@ -0,0 +1,104 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; + +import java.util.List; +import java.util.Objects; + +public class RequestCorrectionProcessedAuctionHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "pb-request-correction-processed-auction-request"; + + private final RequestCorrectionProvider requestCorrectionProvider; + private final ObjectMapper mapper; + + public RequestCorrectionProcessedAuctionHook(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) { + this.requestCorrectionProvider = Objects.requireNonNull(requestCorrectionProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext context) { + + final Config config; + try { + config = moduleConfig(context.accountConfig()); + } catch (PreBidException e) { + return failure(e.getMessage()); + } + + if (config == null || !config.isEnabled()) { + return noAction(); + } + + final BidRequest bidRequest = payload.bidRequest(); + + final List corrections = requestCorrectionProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AuctionRequestPayloadImpl.of( + applyCorrections(initialPayload.bidRequest(), corrections))) + .build(); + + return Future.succeededFuture(invocationResult); + } + + private Config moduleConfig(ObjectNode accountConfig) { + try { + return mapper.treeToValue(accountConfig, Config.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static BidRequest applyCorrections(BidRequest bidRequest, List corrections) { + BidRequest result = bidRequest; + for (Correction correction : corrections) { + result = correction.apply(result); + } + return result; + } + + private Future> failure(String message) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .action(InvocationAction.no_action) + .build()); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java new file mode 100644 index 00000000000..56856d10c16 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class RequestCorrectionProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private RequestCorrectionProvider target; + + @BeforeEach + public void setUp() { + target = new RequestCorrectionProvider(singletonList(correctionProducer)); + } + + @Test + public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(false); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).isEmpty(); + } + + @Test + public void correctionsShouldReturnProducedCorrection() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(true); + + final Correction correction = mock(Correction.class); + given(correctionProducer.produce(any())).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java new file mode 100644 index 00000000000..3a44b7158e3 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java @@ -0,0 +1,132 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InterstitialCorrectionProducerTest { + + private final InterstitialCorrectionProducer target = new InterstitialCorrectionProducer(); + + @Test + public void shouldProduceReturnsFalseIfCorrectionDisabled() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(false) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfThereIsNothingToDo() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(emptyList()) + .app(App.builder().build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfBundleNotAnAndroid() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundle") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", null), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfVersionInvalid() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundleAndroid") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsTrueWhenAllConditionsMatch() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundleAndroid") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isTrue(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java new file mode 100644 index 00000000000..490607a7d5e --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InterstitialCorrectionTest { + + private final InterstitialCorrection target = new InterstitialCorrection(); + + @Test + public void applyShouldCorrectInterstitial() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList( + Imp.builder().instl(0).build(), + Imp.builder().build(), + Imp.builder().instl(1).build())) + .build(); + + // when + final BidRequest result = target.apply(bidRequest); + + // then + assertThat(result) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getInstl) + .containsExactly(0, null, null); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java new file mode 100644 index 00000000000..cb7e3458bef --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java @@ -0,0 +1,125 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class UserAgentCorrectionProducerTest { + + private final UserAgentCorrectionProducer target = new UserAgentCorrectionProducer(); + + @Test + public void shouldProduceReturnsFalseIfCorrectionDisabled() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(false) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfThereIsNothingToDo() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfVersionInvalid() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfDeviceUserAgentDoesNotMatch() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("Blah blah").build()) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsTrueWhenAllConditionsMatch() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("Blah PrebidMobile/1asdf blah").build()) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isTrue(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java new file mode 100644 index 00000000000..c8ed5f6762d --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UserAgentCorrectionTest { + + private final UserAgentCorrection target = new UserAgentCorrection(); + + @Test + public void applyShouldCorrectUserAgent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("blah PrebidMobile/1asdf blah").build()) + .build(); + + // when + final BidRequest result = target.apply(bidRequest); + + // then + assertThat(result) + .extracting(BidRequest::getDevice) + .extracting(Device::getUa) + .isEqualTo("blah blah"); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java new file mode 100644 index 00000000000..8da1ec6a3c3 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java @@ -0,0 +1,52 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.util; + +import org.junit.jupiter.api.Test; + +import static java.lang.Integer.MAX_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +public class VersionUtilTest { + + @Test + public void isVersionLessThanShouldReturnFalseIfVersionGreaterThanRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 2, 3)).isFalse(); + } + + @Test + public void isVersionLessThenShouldReturnFalseIfVersionIsEqualToRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 4, 3)).isFalse(); + } + + @Test + public void isVersionLessThenShouldReturnTrueIfVersionIsLessThanRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 4, 3)).isTrue(); + } + + @Test + public void isVersionLessThenShouldReturnExpectedResults() { + // major + assertThat(VersionUtil.isVersionLessThan("0", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("3", 2, 2, 3)).isFalse(); + + // minor + assertThat(VersionUtil.isVersionLessThan("0." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.0", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.3", 2, 2, 3)).isFalse(); + + // patch + assertThat(VersionUtil.isVersionLessThan("0.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.1." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 2, 3)).isFalse(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java new file mode 100644 index 00000000000..9250e188cce --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java @@ -0,0 +1,120 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class RequestCorrectionProcessedAuctionHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private RequestCorrectionProvider requestCorrectionProvider; + + private RequestCorrectionProcessedAuctionHook target; + + @Mock + private AuctionRequestPayload payload; + + @Mock + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder() + .enabled(true) + .interstitialCorrectionEnabled(true) + .build())); + + target = new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, MAPPER); + } + + @Test + public void callShouldReturnFailedResultOnInvalidConfiguration() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList()))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type"); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionOnDisabledConfig() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder() + .enabled(false) + .interstitialCorrectionEnabled(true) + .build())); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { + // given + given(requestCorrectionProvider.corrections(any(), any())).willReturn(emptyList()); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnUpdate() { + // given + final Correction correction = mock(Correction.class); + given(requestCorrectionProvider.corrections(any(), any())).willReturn(singletonList(correction)); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + }); + } +} diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml new file mode 100644 index 00000000000..a3747db2087 --- /dev/null +++ b/extra/modules/pb-response-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.18.0-SNAPSHOT + + + pb-response-correction + + pb-response-correction + Response correction module + diff --git a/extra/modules/pb-response-correction/src/lombok.config b/extra/modules/pb-response-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-response-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..e119bb59703 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.pb.response.correction.config; + +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrection; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.v1.ResponseCorrectionModule; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + ResponseCorrectionModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class ResponseCorrectionModuleConfiguration { + + @Bean + AppVideoHtmlCorrectionProducer appVideoHtmlCorrectionProducer( + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new AppVideoHtmlCorrectionProducer( + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), logSamplingRate)); + } + + @Bean + ResponseCorrectionProvider responseCorrectionProvider(List correctionProducers) { + return new ResponseCorrectionProvider(correctionProducers); + } + + @Bean + ResponseCorrectionModule responseCorrectionModule(ResponseCorrectionProvider responseCorrectionProvider) { + return new ResponseCorrectionModule(responseCorrectionProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java new file mode 100644 index 00000000000..9bdf2ceea8c --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class ResponseCorrectionProvider { + + private final List correctionProducers; + + public ResponseCorrectionProvider(List correctionProducers) { + this.correctionProducers = Objects.requireNonNull(correctionProducers); + } + + public List corrections(Config config, BidRequest bidRequest) { + return correctionProducers.stream() + .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) + .map(CorrectionProducer::produce) + .toList(); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java new file mode 100644 index 00000000000..06b0990f149 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AppVideoHtmlConfig { + + boolean enabled; + + @JsonProperty("excluded-bidders") + List excludedBidders; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java new file mode 100644 index 00000000000..17cd2453b16 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Config { + + boolean enabled; + + @JsonProperty("app-video-html") + AppVideoHtmlConfig appVideoHtmlConfig; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java new file mode 100644 index 00000000000..3f7abf1c5c5 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +import java.util.List; + +public interface Correction { + + List apply(Config config, List bidderResponses); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..6cd19836b96 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java new file mode 100644 index 00000000000..5b3ee918e86 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java @@ -0,0 +1,138 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +public class AppVideoHtmlCorrection implements Correction { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(AppVideoHtmlCorrection.class)); + + private static final Pattern VAST_XML_PATTERN = Pattern.compile(".*<\\s*VAST\\s+.*", Pattern.CASE_INSENSITIVE); + private static final TypeReference> EXT_BID_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String NATIVE_ADM_MESSAGE = "Bid %s of bidder %s has an JSON ADM, that appears to be native"; + private static final String ADM_WITH_NO_ASSETS_MESSAGE = "Bid %s of bidder %s has a JSON ADM, but without assets"; + private static final String CHANGING_BID_MEDIA_TYPE_MESSAGE = "Bid %s of bidder %s: changing media type to banner"; + + private final ObjectMapper mapper; + private final double logSamplingRate; + + public AppVideoHtmlCorrection(ObjectMapper mapper, double logSamplingRate) { + this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; + } + + @Override + public List apply(Config config, List bidderResponses) { + final Collection excludedBidders = CollectionUtils.emptyIfNull( + config.getAppVideoHtmlConfig().getExcludedBidders()); + + return bidderResponses.stream() + .map(response -> modify(response, excludedBidders)) + .toList(); + } + + private BidderResponse modify(BidderResponse response, Collection excludedBidders) { + final String bidder = response.getBidder(); + if (excludedBidders.contains(bidder)) { + return response; + } + + final BidderSeatBid seatBid = response.getSeatBid(); + final List modifiedBids = seatBid.getBids().stream() + .map(bidderBid -> modifyBid(bidder, bidderBid)) + .toList(); + + return response.with(seatBid.with(modifiedBids)); + } + + private BidderBid modifyBid(String bidder, BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidId = bid.getId(); + final String adm = bid.getAdm(); + + if (adm == null || isVideoWithVastXml(bidderBid.getType(), adm) || hasNativeAdm(adm, bidId, bidder)) { + return bidderBid; + } + + conditionalLogger.warn(CHANGING_BID_MEDIA_TYPE_MESSAGE.formatted(bidId, bidder), logSamplingRate); + + final ExtBidPrebid prebid = parseExtBidPrebid(bid); + + final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .mediaType(BidType.video.getName()) + .build(); + + final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid) + .map(ExtBidPrebid::toBuilder) + .orElseGet(ExtBidPrebid::builder) + .meta(modifiedMeta) + .type(BidType.banner) + .build(); + + final ObjectNode modifiedBidExt = mapper.valueToTree(ExtPrebid.of(modifiedPrebid, null)); + + return bidderBid.toBuilder() + .type(BidType.banner) + .bid(bid.toBuilder().ext(modifiedBidExt).build()) + .build(); + } + + private boolean hasNativeAdm(String adm, String bidId, String bidder) { + final JsonNode admNode; + try { + admNode = mapper.readTree(adm); + } catch (JsonProcessingException e) { + return false; + } + + final boolean hasAssets = admNode.has("assets"); + final String warningMessage = hasAssets + ? NATIVE_ADM_MESSAGE.formatted(bidId, bidder) + : ADM_WITH_NO_ASSETS_MESSAGE.formatted(bidId, bidder); + + conditionalLogger.warn(warningMessage, logSamplingRate); + return hasAssets; + } + + private static boolean isVideoWithVastXml(BidType type, String adm) { + return type == BidType.video && VAST_XML_PATTERN.matcher(adm).matches(); + } + + private ExtBidPrebid parseExtBidPrebid(Bid bid) { + try { + return mapper.convertValue(bid.getExt(), EXT_BID_PREBID_TYPE_REFERENCE).getPrebid(); + } catch (Exception e) { + return null; + } + } + +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java new file mode 100644 index 00000000000..f7a05137bf0 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +public class AppVideoHtmlCorrectionProducer implements CorrectionProducer { + + private final AppVideoHtmlCorrection correctionInstance; + + public AppVideoHtmlCorrectionProducer(AppVideoHtmlCorrection correction) { + this.correctionInstance = correction; + } + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final AppVideoHtmlConfig appVideoHtmlConfig = config.getAppVideoHtmlConfig(); + final boolean enabled = appVideoHtmlConfig != null && appVideoHtmlConfig.isEnabled(); + return enabled && bidRequest.getApp() != null; + } + + @Override + public Correction produce() { + return correctionInstance; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java new file mode 100644 index 00000000000..9f9e8e75659 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -0,0 +1,105 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; +import java.util.Objects; + +public class ResponseCorrectionAllProcessedBidResponsesHook implements AllProcessedBidResponsesHook { + + private static final String CODE = "pb-response-correction-all-processed-bid-responses"; + + private final ResponseCorrectionProvider responseCorrectionProvider; + private final ObjectMapper mapper; + + public ResponseCorrectionAllProcessedBidResponsesHook(ResponseCorrectionProvider responseCorrectionProvider, + ObjectMapper mapper) { + this.responseCorrectionProvider = Objects.requireNonNull(responseCorrectionProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AllProcessedBidResponsesPayload payload, + AuctionInvocationContext context) { + + final Config config; + try { + config = moduleConfig(context.accountConfig()); + } catch (PreBidException e) { + return failure(e.getMessage()); + } + + if (config == null || !config.isEnabled()) { + return noAction(); + } + + final BidRequest bidRequest = context.auctionContext().getBidRequest(); + + final List corrections = responseCorrectionProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AllProcessedBidResponsesPayloadImpl.of( + applyCorrections(initialPayload.bidResponses(), config, corrections))) + .build(); + + return Future.succeededFuture(invocationResult); + } + + private Config moduleConfig(ObjectNode accountConfig) { + try { + return mapper.treeToValue(accountConfig, Config.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static List applyCorrections(List bidderResponses, Config config, List corrections) { + List result = bidderResponses; + for (Correction correction : corrections) { + result = correction.apply(config, result); + } + return result; + } + + private Future> failure(String message) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .action(InvocationAction.no_action) + .build()); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java new file mode 100644 index 00000000000..29e32743201 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Collections; + +public class ResponseCorrectionModule implements Module { + + public static final String CODE = "pb-response-correction"; + + private final Collection> hooks; + + public ResponseCorrectionModule(ResponseCorrectionProvider responseCorrectionProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new ResponseCorrectionAllProcessedBidResponsesHook(responseCorrectionProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java new file mode 100644 index 00000000000..6b8fc33ba95 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ResponseCorrectionProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private ResponseCorrectionProvider target; + + @BeforeEach + public void setUp() { + target = new ResponseCorrectionProvider(singletonList(correctionProducer)); + } + + @Test + public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(false); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).isEmpty(); + } + + @Test + public void correctionsShouldReturnProducedCorrection() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(true); + + final Correction correction = mock(Correction.class); + given(correctionProducer.produce()).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java new file mode 100644 index 00000000000..15305d6bed2 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class AppVideoHtmlCorrectionProducerTest { + + private final AppVideoHtmlCorrection CORRECTION_INSTANCE = + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), 0.1); + + private final AppVideoHtmlCorrectionProducer target = new AppVideoHtmlCorrectionProducer(CORRECTION_INSTANCE); + + @Test + public void produceShouldReturnCorrectionInstance() { + // when & then + assertThat(target.produce()).isSameAs(CORRECTION_INSTANCE); + } + + @Test + public void shouldProduceReturnFalseWhenAppVideoHtmlConfigIsDisabled() { + // given + final Config givenConfig = givenConfig(false); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnFalseWhenBidRequestIsNotAppRequest() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().site(Site.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnTrueWhenConfigIsEnabledAndBidRequestHasApp() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isTrue(); + } + + private static Config givenConfig(boolean enabled) { + return Config.of(true, AppVideoHtmlConfig.of(enabled, null)); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java new file mode 100644 index 00000000000..9b36b0210da --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java @@ -0,0 +1,197 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AppVideoHtmlCorrectionTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + private final AppVideoHtmlCorrection target = new AppVideoHtmlCorrection(MAPPER, 0.1); + + @Test + public void applyShouldNotChangeBidResponsesFromExcludedBidders() { + // given + final Config givenConfig = givenConfig(List.of("bidderA", "bidderB")); + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", null, 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static Config givenConfig(List excludedBidders) { + return Config.of(true, AppVideoHtmlConfig.of(true, excludedBidders)); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenAdmIsNull() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + final BidderBid givenBid = givenBid(null, BidType.video); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of(List.of(givenBid)), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static BidderBid givenBid(String adm, BidType type) { + return givenBid(adm, type, null); + } + + private static BidderBid givenBid(String adm, BidType type, ObjectNode bidExt) { + final Bid bid = Bid.builder().adm(adm).ext(bidExt).build(); + return BidderBid.of(bid, type, "USD"); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidIsVideoAndHasVastXmlInAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid("< \tVAST anything>", BidType.video))), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidHasNativeAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid("{\"field\":1,\"assets\":[{\"id\":2}]}", BidType.video))), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsJsonButNotNative() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.video))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + private static Config givenConfig() { + return Config.of(true, AppVideoHtmlConfig.of(true, null)); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsVastXmlAndTypeIsNotVideo() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.xNative))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndOverwriteMetaTypeToVideoWhenAdmIsNotVastXmlAndTypeIsVideo() { + // given + final Config givenConfig = givenConfig(); + + final ExtBidPrebid givenPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("banner").build()) + .build(); + final ObjectNode givenBidExt = MAPPER.valueToTree(ExtPrebid.of(givenPrebid, null)); + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.video, givenBidExt))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java new file mode 100644 index 00000000000..0e525b06f24 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java @@ -0,0 +1,118 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ResponseCorrectionAllProcessedBidResponsesHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private ResponseCorrectionProvider responseCorrectionProvider; + + private ResponseCorrectionAllProcessedBidResponsesHook target; + + @Mock + private AllProcessedBidResponsesPayload payload; + + @Mock(strictness = Mock.Strictness.LENIENT) + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(true, null))); + given(invocationContext.auctionContext()) + .willReturn(AuctionContext.builder().bidRequest(BidRequest.builder().build()).build()); + + target = new ResponseCorrectionAllProcessedBidResponsesHook(responseCorrectionProvider, MAPPER); + } + + @Test + public void callShouldReturnFailedResultOnInvalidConfiguration() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList()))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type"); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionOnDisabledConfig() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(false, null))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { + // given + given(responseCorrectionProvider.corrections(any(), any())).willReturn(emptyList()); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnUpdate() { + // given + final Correction correction = mock(Correction.class); + given(responseCorrectionProvider.corrections(any(), any())).willReturn(singletonList(correction)); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + }); + } +} diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index d177812c588..601659dbc01 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java index bce3668c0a5..fe3a8a37ac6 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java @@ -20,8 +20,8 @@ public class PbRichmediaFilterModuleConfiguration { @Bean PbRichmediaFilterModule pbRichmediaFilterModule( - @Value("${hooks.modules.pb-richmedia-filter.filter-mraid}") Boolean filterMraid, - @Value("${hooks.modules.pb-richmedia-filter.mraid-script-pattern}") String mraidScriptPattern) { + @Value("${hooks.modules.pb-richmedia-filter.filter-mraid:false}") boolean filterMraid, + @Value("${hooks.modules.pb-richmedia-filter.mraid-script-pattern:#{null}}") String mraidScriptPattern) { final ObjectMapper mapper = ObjectMapperProvider.mapper(); final PbRichMediaFilterProperties globalProperties = PbRichMediaFilterProperties.of( diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java index 208625e715b..499de57b1d8 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java @@ -2,6 +2,8 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -21,7 +23,10 @@ public class BidResponsesMraidFilter { private static final String TAG_STATUS = "success-block"; private static final Map TAG_VALUES = Map.of("richmedia-format", "mraid"); - public MraidFilterResult filterByPattern(String mraidScriptPattern, List responses) { + public MraidFilterResult filterByPattern(String mraidScriptPattern, + List responses, + Map bidRejectionTrackers) { + List filteredResponses = new ArrayList<>(); List analyticsResults = new ArrayList<>(); @@ -41,18 +46,21 @@ public MraidFilterResult filterByPattern(String mraidScriptPattern, List errors = new ArrayList<>(seatBid.getErrors()); - errors.add(BidderError.of( - "Invalid creatives", - BidderError.Type.invalid_creative, - new HashSet<>(rejectedImps))); + errors.add(BidderError.of("Invalid bid", BidderError.Type.invalid_bid, new HashSet<>(rejectedImps))); filteredResponses.add(bidderResponse.with(seatBid.with(validBids, errors))); } } diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java index 3465eb08fc4..c63baeda58c 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java @@ -6,17 +6,17 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -58,7 +58,10 @@ public Future> call( final List responses = allProcessedBidResponsesPayload.bidResponses(); if (BooleanUtils.isTrue(properties.getFilterMraid())) { - final MraidFilterResult filterResult = mraidFilter.filterByPattern(properties.getMraidScriptPattern(), responses); + final MraidFilterResult filterResult = mraidFilter.filterByPattern( + properties.getMraidScriptPattern(), + responses, + auctionInvocationContext.auctionContext().getBidRejectionTrackers()); final InvocationAction action = filterResult.hasRejectedBids() ? InvocationAction.update : InvocationAction.no_action; diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index bb9e887ca02..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 24f793287b5..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java deleted file mode 100644 index e15359f5c14..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java deleted file mode 100644 index b996bcb4355..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java index 1997372757d..0198210d1ca 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java @@ -2,6 +2,8 @@ import com.iab.openrtb.response.Bid; import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -14,6 +16,10 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class BidResponsesMraidFilterTest { @@ -24,41 +30,56 @@ public void filterShouldReturnOriginalBidsWhenNoBidsHaveMraidScriptInAdm() { // given final BidderResponse responseA = givenBidderResponse("bidderA", List.of(givenBid("imp_id", "adm1"))); final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid("imp_id", "adm2"))); + final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class); + final Map givenTrackers = Map.of( + "bidderA", bidRejectionTrackerA, + "bidderB", bidRejectionTrackerB); // when - final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB)); + final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB), givenTrackers); // then assertThat(filterResult.getFilterResult()).containsExactly(responseA, responseB); assertThat(filterResult.getAnalyticsResult()).isEmpty(); assertThat(filterResult.hasRejectedBids()).isFalse(); + + verifyNoInteractions(bidRejectionTrackerA, bidRejectionTrackerB); } @Test public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() { // given - final BidderResponse responseA = givenBidderResponse("bidderA", List.of( - givenBid("imp_id1", "adm1"), - givenBid("imp_id2", "adm2"))); - final BidderResponse responseB = givenBidderResponse("bidderB", List.of( - givenBid("imp_id1", "adm1"), - givenBid("imp_id2", "adm2_mraid.js"))); - final BidderResponse responseC = givenBidderResponse("bidderC", List.of( - givenBid("imp_id1", "adm1_mraid.js"), - givenBid("imp_id2", "adm2_mraid.js"))); + final BidderBid givenBid1 = givenBid("imp_id1", "adm1"); + final BidderBid givenBid2 = givenBid("imp_id2", "adm2"); + final BidderBid givenInvalidBid1 = givenBid("imp_id1", "adm1_mraid.js"); + final BidderBid givenInvalidBid2 = givenBid("imp_id2", "adm2_mraid.js"); + + final BidderResponse responseA = givenBidderResponse("bidderA", List.of(givenBid1, givenBid2)); + final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid1, givenInvalidBid2)); + final BidderResponse responseC = givenBidderResponse("bidderC", List.of(givenInvalidBid1, givenInvalidBid2)); + + final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerC = mock(BidRejectionTracker.class); + final Map givenTrackers = Map.of( + "bidderA", bidRejectionTrackerA, + "bidderB", bidRejectionTrackerB, + "bidderC", bidRejectionTrackerC); // when final MraidFilterResult filterResult = target.filterByPattern( "mraid.js", - List.of(responseA, responseB, responseC)); + List.of(responseA, responseB, responseC), + givenTrackers); // then final BidderResponse expectedResponseA = givenBidderResponse( "bidderA", - List.of(givenBid("imp_id1", "adm1"), givenBid("imp_id2", "adm2"))); + List.of(givenBid1, givenBid2)); final BidderResponse expectedResponseB = givenBidderResponse( "bidderB", - List.of(givenBid("imp_id1", "adm1")), + List.of(givenBid1), List.of(givenError("imp_id2"))); final BidderResponse expectedResponseC = givenBidderResponse( "bidderC", @@ -81,6 +102,13 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() assertThat(filterResult.getAnalyticsResult()) .containsExactlyInAnyOrder(expectedAnalyticsResultB, expectedAnalyticsResultC); assertThat(filterResult.hasRejectedBids()).isTrue(); + + verifyNoInteractions(bidRejectionTrackerA); + verify(bidRejectionTrackerB) + .rejectBids(List.of(givenInvalidBid2), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTrackerC) + .rejectBids(List.of(givenInvalidBid1, givenInvalidBid2), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verifyNoMoreInteractions(bidRejectionTrackerB, bidRejectionTrackerC); } private static BidderResponse givenBidderResponse(String bidder, List bids) { @@ -96,7 +124,7 @@ private static BidderBid givenBid(String impId, String adm) { } private static BidderError givenError(String... rejectedImps) { - return BidderError.of("Invalid creatives", BidderError.Type.invalid_creative, Set.of(rejectedImps)); + return BidderError.of("Invalid bid", BidderError.Type.invalid_bid, Set.of(rejectedImps)); } } diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java index 8e8b67ea289..2a87faec776 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java @@ -7,18 +7,20 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -48,7 +50,7 @@ public class PbRichmediaFilterAllProcessedBidResponsesHookTest { @Mock private AllProcessedBidResponsesPayload allProcessedBidResponsesPayload; - @Mock + @Mock(strictness = LENIENT) private AuctionInvocationContext auctionInvocationContext; @Mock @@ -59,10 +61,15 @@ public class PbRichmediaFilterAllProcessedBidResponsesHookTest { private PbRichmediaFilterAllProcessedBidResponsesHook target; + private static final Map BID_REJECTION_TRACKERS = Map.of( + "bidder", new BidRejectionTracker("bidder", Collections.emptySet(), 0.1)); + @BeforeEach public void setUp() { target = new PbRichmediaFilterAllProcessedBidResponsesHook(ObjectMapperProvider.mapper(), mraidFilter, configResolver); when(configResolver.resolve(any())).thenReturn(PbRichMediaFilterProperties.of(true, "pattern")); + when(auctionInvocationContext.auctionContext()) + .thenReturn(AuctionContext.builder().bidRejectionTrackers(BID_REJECTION_TRACKERS).build()); } @Test @@ -103,7 +110,7 @@ public void callShouldReturnResultWithUpdateActionWhenSomeResponsesWereFilteredO // given final List givenResponses = givenBidderResponses(2); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, List.of(givenAnalyticsResult("bidder", "imp_id")))); // when @@ -126,7 +133,7 @@ public void callShouldReturnResultWithNoActionWhenNothingWereFilteredOut() { // given final List givenResponses = givenBidderResponses(2); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, Collections.emptyList())); // when @@ -150,7 +157,7 @@ public void callShouldReturnResultOfFilteredResponses() { final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); final List expectedResponses = givenBidderResponses(2); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(expectedResponses, Collections.emptyList())); // when @@ -174,7 +181,7 @@ public void callShouldReturnAnalyticsResultsOfRejectedBids() { // given final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of( givenResponses, List.of( @@ -219,7 +226,7 @@ public void callShouldReturnEmptyAnalyticsResultsWhenThereAreNoRejectedBids() { // given final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, Collections.emptyList())); // when diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 0240ae00a38..8f558dec877 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT ../../extra/pom.xml @@ -22,6 +21,9 @@ confiant-ad-quality pb-richmedia-filter fiftyone-devicedetection + pb-response-correction + greenbids-real-time-data + pb-request-correction diff --git a/extra/pom.xml b/extra/pom.xml index f5bd454a313..24db40b91c4 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -1,11 +1,10 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT pom @@ -50,12 +49,13 @@ 2.0.10 3.2.0 2.12.0 - 3.21.7 - 3.17.3 + 3.25.5 + ${protobuf.version} 1.0.7 + 2.26.24 - 3.5.4 + 3.9.1 2.4-M4-groovy-4.0 5.15.0 @@ -213,6 +213,11 @@ geoip2 ${maxmind-client.version} + + software.amazon.awssdk + s3 + ${aws.awssdk.version} + com.google.protobuf protobuf-java @@ -263,6 +268,8 @@ maven-release-plugin ${maven-release-plugin.version} + false + true @{project.version} Prebid Server diff --git a/pom.xml b/pom.xml index 029d2babfad..573a265dd12 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.18.0-SNAPSHOT extra/pom.xml @@ -171,6 +170,10 @@ org.postgresql postgresql + + software.amazon.awssdk + s3 + com.github.ben-manes.caffeine caffeine @@ -329,6 +332,11 @@ mysql test + + org.testcontainers + localstack + test + org.testcontainers postgresql @@ -637,7 +645,7 @@ ${mockserver.version} ${project.version} - 2 + 5 false diff --git a/sample/configs/prebid-config-s3.yaml b/sample/configs/prebid-config-s3.yaml new file mode 100644 index 00000000000..277ad94613c --- /dev/null +++ b/sample/configs/prebid-config-s3.yaml @@ -0,0 +1,60 @@ +status-response: "ok" + +server: + enable-quickack: true + enable-reuseport: true + +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + s3: + accessKeyId: prebid-server-test + secretAccessKey: nq9h6whXQURNL2NnWg3rcMlLMtGGDJeWrdl8hC9g + endpoint: http://localhost:9000 + bucket: prebid-server-configs.example.com # prebid-application-settings + force-path-style: true # virtual bucketing + # region: # if not provided AWS_GLOBAL will be used. Example value: 'eu-central-1' + accounts-dir: accounts + stored-imps-dir: stored-impressions + stored-requests-dir: stored-requests + stored-responses-dir: stored-responses + + in-memory-cache: + cache-size: 10000 + ttl-seconds: 1200 # 20 minutes + s3-update: + refresh-rate: 900000 # Refresh every 15 minutes + timeout: 5000 + +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 + +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false diff --git a/sample/prebid-config-with-51d-dd.yaml b/sample/configs/prebid-config-with-51d-dd.yaml similarity index 93% rename from sample/prebid-config-with-51d-dd.yaml rename to sample/configs/prebid-config-with-51d-dd.yaml index f32674538a3..d523bdbdf11 100644 --- a/sample/prebid-config-with-51d-dd.yaml +++ b/sample/configs/prebid-config-with-51d-dd.yaml @@ -21,10 +21,10 @@ settings: enforce-valid-account: false generate-storedrequest-bidrequest-id: true filesystem: - settings-filename: sample/sample-app-settings.yaml - stored-requests-dir: sample/stored - stored-imps-dir: sample/stored - stored-responses-dir: sample/stored + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample categories-dir: gdpr: default-value: 1 diff --git a/sample/configs/prebid-config-with-module.yaml b/sample/configs/prebid-config-with-module.yaml index 93a6a13d29f..06fc01fe3e2 100644 --- a/sample/configs/prebid-config-with-module.yaml +++ b/sample/configs/prebid-config-with-module.yaml @@ -21,10 +21,10 @@ settings: enforce-valid-account: false generate-storedrequest-bidrequest-id: true filesystem: - settings-filename: sample/sample-app-settings.yaml - stored-requests-dir: sample/stored - stored-imps-dir: sample/stored - stored-responses-dir: sample/stored + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample categories-dir: gdpr: default-value: 1 diff --git a/src/main/docker/run.sh b/src/main/docker/run.sh index 54f73437643..884aa8b3fd1 100755 --- a/src/main/docker/run.sh +++ b/src/main/docker/run.sh @@ -5,4 +5,4 @@ exec java \ -Dspring.config.additional-location=/app/prebid-server/,/app/prebid-server/conf/ \ ${JAVA_OPTS} \ -jar \ - /app/prebid-server/prebid-server.jar + /app/prebid-server/prebid-server.jar "$@" diff --git a/src/main/java/com/iab/openrtb/request/Eid.java b/src/main/java/com/iab/openrtb/request/Eid.java index f8a728e93cb..04892b57cc6 100644 --- a/src/main/java/com/iab/openrtb/request/Eid.java +++ b/src/main/java/com/iab/openrtb/request/Eid.java @@ -1,33 +1,24 @@ package com.iab.openrtb.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; import java.util.List; -/** - * Extended identifiers support in the OpenRTB specification allows buyers - * to use audience data in real-time bidding. This object can contain one - * or more {@link Uid}s from a single source or a technology provider. The - * exchange should ensure that business agreements allow for the sending - * of this data. - */ -@Value(staticConstructor = "of") +@Value +@Builder(toBuilder = true) public class Eid { - /** - * Source or technology provider responsible for the set of included IDs. Expressed as a top-level domain. - */ String source; - /** - * Array of extended ID {@link Uid} objects from the given source. - * Refer to 3.2.28 Extended Identifier UIDs - */ List uids; - /** - * Placeholder for vendor specific extensions to this object - */ + String inserter; + + String matcher; + + Integer mm; + ObjectNode ext; } diff --git a/src/main/java/com/iab/openrtb/request/Uid.java b/src/main/java/com/iab/openrtb/request/Uid.java index 536d5a1e02b..c90e563b9d5 100644 --- a/src/main/java/com/iab/openrtb/request/Uid.java +++ b/src/main/java/com/iab/openrtb/request/Uid.java @@ -1,6 +1,7 @@ package com.iab.openrtb.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; /** @@ -8,7 +9,8 @@ * extended identifiers. The exchange should ensure that business * agreements allow for the sending of this data. */ -@Value(staticConstructor = "of") +@Value +@Builder(toBuilder = true) public class Uid { /** diff --git a/src/main/java/com/iab/openrtb/request/Video.java b/src/main/java/com/iab/openrtb/request/Video.java index f967886bf89..369d576a3ac 100644 --- a/src/main/java/com/iab/openrtb/request/Video.java +++ b/src/main/java/com/iab/openrtb/request/Video.java @@ -254,6 +254,12 @@ public class Video { */ List companiontype; + /** + * Indicates pod deduplication settings that will be applied to bid responses. Refer to + * List: Pod Deduplication in AdCOM 1.0. + */ + List poddedupe; + /** * An array of objects (Section 3.2.35) * indicating the floor prices for video creatives of various durations that the buyer may bid with. diff --git a/src/main/java/org/prebid/server/analytics/model/AmpEvent.java b/src/main/java/org/prebid/server/analytics/model/AmpEvent.java index cf33545a332..fc3e96c913c 100644 --- a/src/main/java/org/prebid/server/analytics/model/AmpEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/AmpEvent.java @@ -13,7 +13,7 @@ /** * Represents a transaction at /openrtb2/amp endpoint. */ -@Builder +@Builder(toBuilder = true) @Value public class AmpEvent { diff --git a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java index fa2b48746aa..10db83bec8a 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java +++ b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java @@ -28,6 +28,7 @@ import org.prebid.server.auction.privacy.enforcement.TcfEnforcement; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -37,6 +38,8 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.StreamUtil; import java.util.Collections; @@ -44,13 +47,11 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -/** - * Class dispatches event processing to all enabled reporters. - */ public class AnalyticsReporterDelegator { private static final Logger logger = LoggerFactory.getLogger(AnalyticsReporterDelegator.class); @@ -63,6 +64,8 @@ public class AnalyticsReporterDelegator { private final UserFpdActivityMask mask; private final Metrics metrics; private final double logSamplingRate; + private final Set globalEnabledAdapters; + private final JacksonMapper mapper; private final Set reporterVendorIds; private final Set reporterNames; @@ -72,7 +75,9 @@ public AnalyticsReporterDelegator(Vertx vertx, TcfEnforcement tcfEnforcement, UserFpdActivityMask userFpdActivityMask, Metrics metrics, - double logSamplingRate) { + double logSamplingRate, + Set globalEnabledAdapters, + JacksonMapper mapper) { this.vertx = Objects.requireNonNull(vertx); this.delegates = Objects.requireNonNull(delegates); @@ -80,6 +85,10 @@ public AnalyticsReporterDelegator(Vertx vertx, this.mask = Objects.requireNonNull(userFpdActivityMask); this.metrics = Objects.requireNonNull(metrics); this.logSamplingRate = logSamplingRate; + this.globalEnabledAdapters = CollectionUtils.isEmpty(globalEnabledAdapters) + ? Collections.emptySet() + : globalEnabledAdapters; + this.mapper = Objects.requireNonNull(mapper); reporterVendorIds = delegates.stream().map(AnalyticsReporter::vendorId).collect(Collectors.toSet()); reporterNames = delegates.stream().map(AnalyticsReporter::name).collect(Collectors.toSet()); @@ -163,11 +172,14 @@ private static boolean isNotEmptyObjectNode(JsonNode analytics) { return analytics != null && analytics.isObject() && !analytics.isEmpty(); } - private static boolean isAllowedAdapter(T event, String adapter) { + private boolean isAllowedAdapter(T event, String adapter) { final ActivityInfrastructure activityInfrastructure; final ActivityInvocationPayload activityInvocationPayload; switch (event) { case AuctionEvent auctionEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, auctionEvent.getAuctionContext())) { + return false; + } final AuctionContext auctionContext = auctionEvent.getAuctionContext(); activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; activityInvocationPayload = auctionContext != null @@ -177,6 +189,10 @@ private static boolean isAllowedAdapter(T event, String adapter) { : null; } case AmpEvent ampEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, ampEvent.getAuctionContext())) { + return false; + } + final AuctionContext auctionContext = ampEvent.getAuctionContext(); activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; activityInvocationPayload = auctionContext != null @@ -186,9 +202,19 @@ private static boolean isAllowedAdapter(T event, String adapter) { : null; } case NotificationEvent notificationEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, notificationEvent.getAccount())) { + return false; + } activityInfrastructure = notificationEvent.getActivityInfrastructure(); activityInvocationPayload = activityInvocationPayload(adapter); } + case VideoEvent videoEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, videoEvent.getAuctionContext())) { + return false; + } + activityInfrastructure = null; + activityInvocationPayload = null; + } case null, default -> { activityInfrastructure = null; activityInvocationPayload = null; @@ -198,6 +224,32 @@ private static boolean isAllowedAdapter(T event, String adapter) { return isAllowedActivity(activityInfrastructure, Activity.REPORT_ANALYTICS, activityInvocationPayload); } + private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, AuctionContext auctionContext) { + return isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, + Optional.ofNullable(auctionContext) + .map(AuctionContext::getAccount) + .orElse(null)); + } + + private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, Account account) { + final Map modules = Optional.ofNullable(account) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .orElse(null); + + if (modules != null && modules.containsKey(adapter)) { + final ObjectNode moduleConfig = modules.get(adapter); + + if (moduleConfig == null || !moduleConfig.has("enabled")) { + return false; + } + + return !moduleConfig.get("enabled").asBoolean(); + } + + return !globalEnabledAdapters.contains(adapter); + } + private static ActivityInvocationPayload activityInvocationPayload(String adapterName) { return ActivityInvocationPayloadImpl.of(ComponentType.ANALYTICS, adapterName); } @@ -299,7 +351,8 @@ private static ObjectNode prepareAnalytics(ObjectNode analytics, String adapterN private void processEventByReporter(AnalyticsReporter analyticsReporter, T event) { final String reporterName = analyticsReporter.name(); - analyticsReporter.processEvent(event) + + analyticsReporter.processEvent(updateEventIfRequired(event, analyticsReporter.name())) .map(ignored -> processSuccess(event, reporterName)) .otherwise(exception -> processFail(exception, event, reporterName)); } @@ -335,4 +388,90 @@ private void updateMetricsByEventType(T event, String analyticsCode, MetricN metrics.updateAnalyticEventMetric(analyticsCode, eventType, result); } + + private T updateEventIfRequired(T event, String adapter) { + switch (event) { + case AuctionEvent auctionEvent -> { + final AuctionContext auctionContext = updateAuctionContext(auctionEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) auctionEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case AmpEvent ampEvent -> { + final AuctionContext auctionContext = updateAuctionContext(ampEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) ampEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case VideoEvent videoEvent -> { + final AuctionContext auctionContext = updateAuctionContext(videoEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) videoEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case null, default -> { + return event; + } + } + } + + private AuctionContext updateAuctionContext(AuctionContext context, String adapterName) { + final Map modules = Optional.ofNullable(context) + .map(AuctionContext::getAccount) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .orElse(null); + + if (modules != null && modules.containsKey(adapterName)) { + final ObjectNode moduleConfig = modules.get(adapterName); + if (moduleConfigContainsAdapterSpecificData(moduleConfig)) { + final ExtRequestPrebid extRequestPrebid = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .orElse(null); + + final JsonNode analyticsNode = extRequestPrebid != null ? extRequestPrebid.getAnalytics() : null; + + if (analyticsNode != null && analyticsNode.isObject()) { + final ObjectNode adapterNode = Optional.ofNullable((ObjectNode) analyticsNode.get(adapterName)) + .orElse(mapper.mapper().createObjectNode()); + + moduleConfig.fields().forEachRemaining(entry -> { + final String fieldName = entry.getKey(); + if (!"enabled".equals(fieldName) && !adapterNode.has(fieldName)) { + adapterNode.set(fieldName, entry.getValue()); + } + }); + + ((ObjectNode) analyticsNode).set(adapterName, adapterNode); + final ExtRequestPrebid updatedPrebid = extRequestPrebid.toBuilder() + .analytics(analyticsNode) + .build(); + final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); + final BidRequest updatedBidRequest = context.getBidRequest().toBuilder() + .ext(updatedExtRequest) + .build(); + return context.toBuilder() + .bidRequest(updatedBidRequest) + .build(); + } + } + } + + return null; + } + + private boolean moduleConfigContainsAdapterSpecificData(ObjectNode moduleConfig) { + if (moduleConfig != null) { + final Iterator fieldNames = moduleConfig.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (!"enabled".equals(fieldName)) { + return true; + } + } + } + + return false; + } } diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java new file mode 100644 index 00000000000..ed99c241ee5 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -0,0 +1,273 @@ +package org.prebid.server.analytics.reporter.agma; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iabtcf.decoder.TCString; +import com.iabtcf.utils.IntIterable; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; +import org.prebid.server.analytics.reporter.agma.model.AgmaEvent; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.Initializable; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.zip.GZIPOutputStream; + +public class AgmaAnalyticsReporter implements AnalyticsReporter, Initializable { + + private static final Logger logger = LoggerFactory.getLogger(AgmaAnalyticsReporter.class); + + private final String url; + private final boolean compressToGzip; + private final long bufferTimeoutMs; + private final long httpTimeoutMs; + + private final EventBuffer buffer; + + private final Map accounts; + + private final Vertx vertx; + private final JacksonMapper jacksonMapper; + private final HttpClient httpClient; + private final Clock clock; + private final MultiMap headers; + + public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper jacksonMapper, + Clock clock, + HttpClient httpClient, + Vertx vertx) { + + this.accounts = agmaAnalyticsProperties.getAccounts(); + + this.url = HttpUtil.validateUrl(agmaAnalyticsProperties.getUrl()); + this.bufferTimeoutMs = agmaAnalyticsProperties.getBufferTimeoutMs(); + this.httpTimeoutMs = agmaAnalyticsProperties.getHttpTimeoutMs(); + this.compressToGzip = agmaAnalyticsProperties.isGzip(); + + this.buffer = new EventBuffer<>( + agmaAnalyticsProperties.getMaxEventsCount(), + agmaAnalyticsProperties.getBufferSize()); + + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.vertx = Objects.requireNonNull(vertx); + this.clock = Objects.requireNonNull(clock); + this.headers = makeHeaders(Objects.requireNonNull(prebidVersionProvider)); + } + + @Override + public void initialize(Promise initializePromise) { + vertx.setPeriodic(bufferTimeoutMs, ignored -> sendEvents(buffer.pollAll())); + initializePromise.complete(); + } + + @Override + public Future processEvent(T event) { + final Pair contextAndType = switch (event) { + case AuctionEvent auctionEvent -> Pair.of(auctionEvent.getAuctionContext(), "auction"); + case AmpEvent ampEvent -> Pair.of(ampEvent.getAuctionContext(), "amp"); + case VideoEvent videoEvent -> Pair.of(videoEvent.getAuctionContext(), "video"); + case null, default -> null; + }; + + if (contextAndType == null) { + return Future.succeededFuture(); + } + + final AuctionContext auctionContext = contextAndType.getLeft(); + final String eventType = contextAndType.getRight(); + if (auctionContext == null) { + return Future.succeededFuture(); + } + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final TimeoutContext timeoutContext = auctionContext.getTimeoutContext(); + final PrivacyContext privacyContext = auctionContext.getPrivacyContext(); + + if (!allowedToSendEvent(bidRequest, privacyContext)) { + return Future.succeededFuture(); + } + + final String accountCode = Optional.ofNullable(bidRequest) + .map(AgmaAnalyticsReporter::getPublisherId) + .map(accounts::get) + .orElse(null); + + if (accountCode == null) { + return Future.succeededFuture(); + } + + final AgmaEvent agmaEvent = AgmaEvent.builder() + .eventType(eventType) + .accountCode(accountCode) + .requestId(bidRequest.getId()) + .app(bidRequest.getApp()) + .site(bidRequest.getSite()) + .device(bidRequest.getDevice()) + .user(bidRequest.getUser()) + .startTime(ZonedDateTime.ofInstant( + Instant.ofEpochMilli(timeoutContext.getStartTime()), clock.getZone())) + .build(); + + final String eventString = jacksonMapper.encodeToString(agmaEvent); + buffer.put(eventString, eventString.length()); + sendEvents(buffer.pollToFlush()); + return Future.succeededFuture(); + } + + private boolean allowedToSendEvent(BidRequest bidRequest, PrivacyContext privacyContext) { + final TCString consent = Optional.ofNullable(privacyContext) + .map(PrivacyContext::getTcfContext) + .map(TcfContext::getConsent) + .or(() -> Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ExtUser::getConsent) + .map(AgmaAnalyticsReporter::decodeConsent)) + .orElse(null); + + if (consent == null) { + return false; + } + + final IntIterable purposesConsent = consent.getPurposesConsent(); + final IntIterable vendorConsent = consent.getVendorConsent(); + + final boolean isPurposeAllowed = purposesConsent.contains(PurposeCode.NINE.code()); + final boolean isVendorAllowed = vendorConsent.contains(vendorId()); + return isPurposeAllowed && isVendorAllowed; + } + + private static TCString decodeConsent(String consent) { + try { + return TCString.decode(consent); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static String getPublisherId(BidRequest bidRequest) { + final Site site = bidRequest.getSite(); + final App app = bidRequest.getApp(); + + final String publisherId = Optional.ofNullable(site).map(Site::getPublisher).map(Publisher::getId) + .or(() -> Optional.ofNullable(app).map(App::getPublisher).map(Publisher::getId)) + .orElse(null); + final String appSiteId = Optional.ofNullable(site).map(Site::getId) + .or(() -> Optional.ofNullable(app).map(App::getId)) + .or(() -> Optional.ofNullable(app).map(App::getBundle)) + .orElse(null); + + if (publisherId == null && appSiteId == null) { + return null; + } + + return StringUtils.isNotBlank(appSiteId) + ? String.format("%s_%s", StringUtils.defaultString(publisherId), appSiteId) + : publisherId; + } + + private void sendEvents(List events) { + if (events.isEmpty()) { + return; + } + final String payload = preparePayload(events); + final Future responseFuture = compressToGzip + ? httpClient.request(HttpMethod.POST, url, headers, gzip(payload), httpTimeoutMs) + : httpClient.request(HttpMethod.POST, url, headers, payload, httpTimeoutMs); + + responseFuture.onComplete(this::handleReportResponse); + } + + private static String preparePayload(List events) { + return "[" + String.join(",", events) + "]"; + } + + private static byte[] gzip(String value) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { + + gzip.write(value.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + + return obj.toByteArray(); + } catch (IOException e) { + throw new PreBidException("[agmaAnalytics] failed to compress, skip the events : " + e.getMessage()); + } + } + + private void handleReportResponse(AsyncResult result) { + if (result.failed()) { + logger.error("[agmaAnalytics] Failed to send events to endpoint {} with a reason: {}", + url, result.cause().getMessage()); + } else { + final HttpClientResponse httpClientResponse = result.result(); + final int statusCode = httpClientResponse.getStatusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + logger.error("[agmaAnalytics] Wrong code received {} instead of 200", statusCode); + } + } + } + + private MultiMap makeHeaders(PrebidVersionProvider versionProvider) { + final MultiMap headers = MultiMap.caseInsensitiveMultiMap() + .add(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) + .add(HttpUtil.X_PREBID_HEADER, versionProvider.getNameVersionRecord()); + + if (compressToGzip) { + headers.add(HttpHeaders.CONTENT_ENCODING, HttpHeaderValues.GZIP); + } + + return headers; + } + + @Override + public int vendorId() { + return 1122; + } + + @Override + public String name() { + return "agmaAnalytics"; + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java b/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java new file mode 100644 index 00000000000..d291fa0ba1c --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java @@ -0,0 +1,59 @@ +package org.prebid.server.analytics.reporter.agma; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class EventBuffer { + + private final Lock lock = new ReentrantLock(true); + + private List events = new ArrayList<>(); + + private long byteSize = 0; + + private final long maxEvents; + + private final long maxBytes; + + public EventBuffer(long maxEvents, long maxBytes) { + this.maxEvents = maxEvents; + this.maxBytes = maxBytes; + } + + public void put(T event, long eventSize) { + lock.lock(); + events.addLast(event); + byteSize += eventSize; + lock.unlock(); + } + + public List pollToFlush() { + List toFlush = Collections.emptyList(); + + lock.lock(); + if (events.size() >= maxEvents || byteSize >= maxBytes) { + toFlush = events; + reset(); + } + lock.unlock(); + + return toFlush; + } + + public List pollAll() { + lock.lock(); + final List polled = events; + reset(); + lock.unlock(); + + return polled; + } + + private void reset() { + byteSize = 0; + events = new ArrayList<>(); + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java new file mode 100644 index 00000000000..1d5a994dbe9 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class AgmaAccountAnalyticsProperties { + + String code; + + String publisherId; + + String siteAppId; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java new file mode 100644 index 00000000000..c1d5ed0c4ed --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java @@ -0,0 +1,26 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class AgmaAnalyticsProperties { + + String url; + + boolean gzip; + + Integer bufferSize; + + Integer maxEventsCount; + + Long bufferTimeoutMs; + + Long httpTimeoutMs; + + Map accounts; + +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java new file mode 100644 index 00000000000..51e385744bf --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java @@ -0,0 +1,38 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; + +@Value +@Builder +public class AgmaEvent { + + @JsonProperty("type") + String eventType; + + @JsonProperty("id") + String requestId; + + @JsonProperty("code") + String accountCode; + + Site site; + + App app; + + Device device; + + User user; + + //format 2023-02-01T00:00:00Z + @JsonProperty("created_at") + ZonedDateTime startTime; + +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java index 5b87a0d4112..65de3f02ebc 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java @@ -21,6 +21,7 @@ import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; @@ -29,9 +30,19 @@ import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsSource; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode; import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.Logger; @@ -50,6 +61,8 @@ import java.time.Clock; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -110,9 +123,13 @@ public Future processEvent(T event) { return Future.succeededFuture(); } - final String greenbidsId = UUID.randomUUID().toString(); final String billingId = UUID.randomUUID().toString(); + final Map analyticsResultFromAnalyticsTag = extractAnalyticsResultFromAnalyticsTag( + auctionContext); + + final String greenbidsId = greenbidsId(analyticsResultFromAnalyticsTag); + if (!isSampled(greenbidsBidRequestExt.getGreenbidsSampling(), greenbidsId)) { return Future.succeededFuture(); } @@ -124,7 +141,8 @@ public Future processEvent(T event) { bidResponse, greenbidsId, billingId, - greenbidsBidRequestExt); + greenbidsBidRequestExt, + analyticsResultFromAnalyticsTag); commonMessageJson = jacksonMapper.encodeToString(commonMessage); } catch (PreBidException e) { return Future.failedFuture(e); @@ -162,6 +180,10 @@ private GreenbidsPrebidExt parseBidRequestExt(BidRequest bidRequest) { .orElse(null); } + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) { try { return jacksonMapper.mapper().treeToValue(adapterNode, GreenbidsPrebidExt.class); @@ -170,8 +192,62 @@ private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) { } } - private boolean isNotEmptyObjectNode(JsonNode analytics) { - return analytics != null && analytics.isObject() && !analytics.isEmpty(); + private Map extractAnalyticsResultFromAnalyticsTag(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getStageOutcomes) + .map(stages -> stages.get(Stage.processed_auction_request)) + .stream() + .flatMap(Collection::stream) + .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hook -> "greenbids-real-time-data".equals(hook.getHookId().getModuleCode())) + .map(HookExecutionOutcome::getAnalyticsTags) + .map(Tags::activities) + .flatMap(Collection::stream) + .filter(activity -> "greenbids-filter".equals(activity.name())) + .map(Activity::results) + .map(List::getFirst) + .map(Result::values) + .map(this::parseAnalyticsResult) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (existing, replacement) -> existing)); + } + + private Map parseAnalyticsResult(ObjectNode analyticsResult) { + try { + final Map parsedAnalyticsResult = new HashMap<>(); + final Iterator> fields = analyticsResult.fields(); + + while (fields.hasNext()) { + final Map.Entry field = fields.next(); + final String impId = field.getKey(); + final JsonNode explorationResultNode = field.getValue(); + final Ortb2ImpExtResult ortb2ImpExtResult = jacksonMapper.mapper() + .treeToValue(explorationResultNode, Ortb2ImpExtResult.class); + parsedAnalyticsResult.put(impId, ortb2ImpExtResult); + } + + return parsedAnalyticsResult; + } catch (JsonProcessingException e) { + throw new PreBidException("Analytics result parsing error", e); + } + } + + private String greenbidsId(Map analyticsResultFromAnalyticsTag) { + return Optional.ofNullable(analyticsResultFromAnalyticsTag) + .map(Map::values) + .map(Collection::stream) + .flatMap(Stream::findFirst) + .map(Ortb2ImpExtResult::getGreenbids) + .map(ExplorationResult::getFingerprint) + .orElseGet(() -> UUID.randomUUID().toString()); } private Future processAnalyticServerResponse(HttpClientResponse response) { @@ -213,7 +289,8 @@ private CommonMessage createBidMessage( BidResponse bidResponse, String greenbidsId, String billingId, - GreenbidsPrebidExt greenbidsImpExt) { + GreenbidsPrebidExt greenbidsImpExt, + Map analyticsResultFromAnalyticsTag) { final Optional bidRequest = Optional.ofNullable(auctionContext.getBidRequest()); final List imps = bidRequest @@ -231,8 +308,10 @@ private CommonMessage createBidMessage( final Map seatsWithNonBids = getSeatsWithNonBids(auctionContext); - final List adUnitsWithBidResponses = imps.stream().map(imp -> createAdUnit( - imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur())).toList(); + final List adUnitsWithBidResponses = imps.stream().map(imp -> + createAdUnit( + imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur(), analyticsResultFromAnalyticsTag)) + .toList(); final String auctionId = bidRequest .map(BidRequest::getId) @@ -283,7 +362,7 @@ private static Map getSeatsWithNonBids(AuctionContext auctionCon } private static SeatNonBid toSeatNonBid(String bidder, BidRejectionTracker bidRejectionTracker) { - final List nonBids = bidRejectionTracker.getRejectionReasons().entrySet().stream() + final List nonBids = bidRejectionTracker.getRejectedImps().entrySet().stream() .map(entry -> NonBid.of(entry.getKey(), entry.getValue())) .toList(); @@ -294,7 +373,8 @@ private GreenbidsAdUnit createAdUnit( Imp imp, Map seatsWithBids, Map seatsWithNonBids, - String currency) { + String currency, + Map analyticsResultFromAnalyticsTag) { final ExtBanner extBanner = getExtBanner(imp.getBanner()); final Video video = imp.getVideo(); final Native nativeObject = imp.getXNative(); @@ -317,11 +397,17 @@ private GreenbidsAdUnit createAdUnit( final List bids = extractBidders( imp.getId(), seatsWithBids, seatsWithNonBids, impExtPrebid, currency); + final Ortb2ImpResult ortb2ImpResult = Optional.ofNullable(analyticsResultFromAnalyticsTag) + .map(analyticsResult -> analyticsResult.get(imp.getId())) + .map(Ortb2ImpResult::of) + .orElse(null); + return GreenbidsAdUnit.builder() .code(adUnitCode) .unifiedCode(greenbidsUnifiedCode) .mediaTypes(mediaTypes) .bids(bids) + .ortb2ImpResult(ortb2ImpResult) .build(); } diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java new file mode 100644 index 00000000000..48a2a0e8038 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java @@ -0,0 +1,18 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExplorationResult { + + String fingerprint; + + @JsonProperty("keptInAuction") + Map keptInAuction; + + @JsonProperty("isExploration") + Boolean isExploration; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java index ce3ae13231a..52f0ebab684 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java @@ -19,4 +19,7 @@ public class GreenbidsAdUnit { MediaTypes mediaTypes; List bids; + + @JsonProperty("ortb2Imp") + Ortb2ImpResult ortb2ImpResult; } diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java index 2e47e071bec..f65e2f1bf80 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java @@ -42,7 +42,7 @@ public static GreenbidsBid ofBid(String seat, Bid bid, JsonNode params, String c public static GreenbidsBid ofNonBid(String seat, NonBid nonBid, JsonNode params, String currency) { return GreenbidsBid.builder() .bidder(seat) - .isTimeout(nonBid.getStatusCode() == BidRejectionReason.TIMED_OUT) + .isTimeout(nonBid.getStatusCode() == BidRejectionReason.ERROR_TIMED_OUT) .hasBid(false) .params(params) .currency(currency) diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java new file mode 100644 index 00000000000..c6cc8350bd8 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java @@ -0,0 +1,11 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Ortb2ImpExtResult { + + ExplorationResult greenbids; + + String tid; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java new file mode 100644 index 00000000000..377bd3c677a --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java @@ -0,0 +1,9 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Ortb2ImpResult { + + Ortb2ImpExtResult ext; +} diff --git a/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java new file mode 100644 index 00000000000..15344b28576 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java @@ -0,0 +1,125 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class AnalyticsTagsEnricher { + + private AnalyticsTagsEnricher() { + } + + public static AuctionContext enrichWithAnalyticsTags(AuctionContext context) { + final boolean clientDetailsEnabled = isClientDetailsEnabled(context); + if (!clientDetailsEnabled) { + return context; + } + + final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::isAllowClientDetails) + .orElse(false); + + if (!allowClientDetails) { + return addClientDetailsWarning(context); + } + + final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); + + if (extAnalyticsTags == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .analytics(ExtAnalytics.of(extAnalyticsTags)) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static boolean isClientDetailsEnabled(AuctionContext context) { + final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .orElse(null); + + if (notObjectNode(analytics)) { + return false; + } + + final JsonNode options = analytics.get("options"); + if (notObjectNode(options)) { + return false; + } + + final JsonNode enableClientDetails = options.get("enableclientdetails"); + return enableClientDetails != null + && enableClientDetails.isBoolean() + && enableClientDetails.asBoolean(); + } + + private static boolean notObjectNode(JsonNode jsonNode) { + return jsonNode == null || !jsonNode.isObject(); + } + + private static AuctionContext addClientDetailsWarning(AuctionContext context) { + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + + final Map> warnings = ext + .map(ExtBidResponse::getWarnings) + .orElse(Collections.emptyMap()); + final List prebidWarnings = ObjectUtils.defaultIfNull( + warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), + Collections.emptyList()); + + final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); + updatedPrebidWarnings.add(ExtBidderError.of( + BidderError.Type.generic.getCode(), + "analytics.options.enableclientdetails not enabled for account")); + final Map> updatedWarnings = new HashMap<>(warnings); + updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .warnings(updatedWarnings) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } +} diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index b843b7f07e4..86fe28fdcbe 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -18,6 +18,7 @@ import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -51,7 +52,7 @@ import org.prebid.server.events.EventsService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; @@ -94,6 +95,7 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountTargetingConfig; import org.prebid.server.settings.model.VideoStoredDataResult; +import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.util.StreamUtil; import org.prebid.server.vast.VastModifier; @@ -122,6 +124,8 @@ public class BidResponseCreator { private static final Integer MAX_TARGETING_KEY_LENGTH = 11; private static final String DEFAULT_TARGETING_KEY_PREFIX = "hb"; public static final String DEFAULT_DEBUG_KEY = "prebid"; + private static final String TARGETING_ENV_APP_VALUE = "mobile-app"; + private static final String TARGETING_ENV_AMP_VALUE = "amp"; private final CoreCacheService coreCacheService; private final BidderCatalog bidderCatalog; @@ -136,6 +140,7 @@ public class BidResponseCreator { private final Clock clock; private final JacksonMapper mapper; private final CacheTtl mediaTypeCacheTtl; + private final CacheDefaultTtlProperties cacheDefaultProperties; private final String cacheHost; private final String cachePath; @@ -153,7 +158,8 @@ public BidResponseCreator(CoreCacheService coreCacheService, int truncateAttrChars, Clock clock, JacksonMapper mapper, - CacheTtl mediaTypeCacheTtl) { + CacheTtl mediaTypeCacheTtl, + CacheDefaultTtlProperties cacheDefaultProperties) { this.coreCacheService = Objects.requireNonNull(coreCacheService); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); @@ -168,6 +174,7 @@ public BidResponseCreator(CoreCacheService coreCacheService, this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); this.mediaTypeCacheTtl = Objects.requireNonNull(mediaTypeCacheTtl); + this.cacheDefaultProperties = Objects.requireNonNull(cacheDefaultProperties); cacheHost = Objects.requireNonNull(coreCacheService.getEndpointHost()); cachePath = Objects.requireNonNull(coreCacheService.getEndpointPath()); @@ -189,10 +196,11 @@ Future createOnSkippedAuction(AuctionContext auctionContext, List cur = bidRequest.getCur(); final BidResponse bidResponse = BidResponse.builder() .id(bidRequest.getId()) - .cur(Stream.ofNullable(bidRequest.getCur()).flatMap(Collection::stream).findFirst().orElse(null)) - .seatbid(Optional.ofNullable(seatBids).orElse(Collections.emptyList())) + .cur(CollectionUtils.isNotEmpty(cur) ? cur.getFirst() : null) + .seatbid(ListUtils.emptyIfNull(seatBids)) .ext(extBidResponse) .build(); @@ -432,8 +440,8 @@ private BidInfo toBidInfo(Bid bid, .bidType(type) .bidder(bidder) .correspondingImp(correspondingImp) - .ttl(resolveBannerTtl(bid, correspondingImp, cacheInfo, account)) - .videoTtl(type == BidType.video ? resolveVideoTtl(bid, correspondingImp, cacheInfo, account) : null) + .ttl(resolveTtl(bid, type, correspondingImp, cacheInfo, account)) + .vastTtl(type == BidType.video ? resolveVastTtl(bid, correspondingImp, cacheInfo, account) : null) .category(categoryMappingResult.getCategory(bid)) .satisfiedPriority(categoryMappingResult.isBidSatisfiesPriority(bid)) .build(); @@ -453,31 +461,43 @@ private static Optional correspondingImp(String impId, List imps) { .findFirst(); } - private Integer resolveBannerTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { - final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + private Integer resolveTtl(Bid bid, BidType type, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { final Integer bidTtl = bid.getExp(); final Integer impTtl = imp != null ? imp.getExp() : null; + final Integer requestTtl = cacheInfo.getCacheBidsTtl(); - return ObjectUtils.firstNonNull( - bidTtl, - impTtl, - cacheInfo.getCacheBidsTtl(), - accountAuctionConfig != null ? accountAuctionConfig.getBannerCacheTtl() : null, - mediaTypeCacheTtl.getBannerCacheTtl()); + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer accountTtl = accountAuctionConfig != null ? switch (type) { + case banner -> accountAuctionConfig.getBannerCacheTtl(); + case video -> accountAuctionConfig.getVideoCacheTtl(); + case audio, xNative -> null; + } : null; + final Integer mediaTypeTtl = switch (type) { + case banner -> mediaTypeCacheTtl.getBannerCacheTtl(); + case video -> mediaTypeCacheTtl.getVideoCacheTtl(); + case audio, xNative -> null; + }; + + final Integer defaultTtl = switch (type) { + case banner -> cacheDefaultProperties.getBannerTtl(); + case video -> cacheDefaultProperties.getVideoTtl(); + case audio -> cacheDefaultProperties.getAudioTtl(); + case xNative -> cacheDefaultProperties.getNativeTtl(); + }; + + return ObjectUtils.firstNonNull(bidTtl, impTtl, requestTtl, accountTtl, mediaTypeTtl, defaultTtl); } - private Integer resolveVideoTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { + private Integer resolveVastTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { final AccountAuctionConfig accountAuctionConfig = account.getAuction(); - final Integer bidTtl = bid.getExp(); - final Integer impTtl = imp != null ? imp.getExp() : null; - return ObjectUtils.firstNonNull( - bidTtl, - impTtl, + bid.getExp(), + imp != null ? imp.getExp() : null, cacheInfo.getCacheVideoBidsTtl(), accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null, - mediaTypeCacheTtl.getVideoCacheTtl()); + mediaTypeCacheTtl.getVideoCacheTtl(), + cacheDefaultProperties.getVideoTtl()); } private Future> invokeProcessedBidderResponseHooks(List bidderResponses, @@ -1325,13 +1345,11 @@ private Bid toBid(BidInfo bidInfo, final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; - final boolean isApp = bidRequest.getApp() != null; - final Map targetingKeywords; final String bidderCode = targetingInfo.getBidderCode(); if (shouldIncludeTargetingInResponse(targeting, bidInfo.getTargetingInfo())) { final TargetingKeywordsCreator keywordsCreator = resolveKeywordsCreator( - bidType, targeting, isApp, bidRequest, account, bidWarnings); + bidType, targeting, bidRequest, account, bidWarnings); final boolean isWinningBid = targetingInfo.isWinningBid(); final String categoryDuration = bidInfo.getCategory(); @@ -1367,7 +1385,7 @@ private Bid toBid(BidInfo bidInfo, final Integer ttl = Optional.ofNullable(cacheInfo) .map(info -> ObjectUtils.max(cacheInfo.getTtl(), cacheInfo.getVideoTtl())) - .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVideoTtl())); + .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVastTtl())); return bid.toBuilder() .ext(updatedBidExt) @@ -1552,16 +1570,15 @@ private Events createEvents(String bidder, private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { final Map keywordsCreatorByBidType = - keywordsCreatorByBidType(targeting, isApp, bidRequest, account, bidWarnings); + keywordsCreatorByBidType(targeting, bidRequest, account, bidWarnings); return keywordsCreatorByBidType.getOrDefault( - bidType, keywordsCreator(targeting, isApp, bidRequest, account, bidWarnings)); + bidType, keywordsCreator(targeting, bidRequest, account, bidWarnings)); } /** @@ -1569,7 +1586,6 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, * instance if it is present. */ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1577,7 +1593,7 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, final JsonNode priceGranularityNode = targeting.getPricegranularity(); return priceGranularityNode == null || priceGranularityNode.isNull() ? null - : createKeywordsCreator(targeting, isApp, priceGranularityNode, bidRequest, account, bidWarnings); + : createKeywordsCreator(targeting, priceGranularityNode, bidRequest, account, bidWarnings); } /** @@ -1586,7 +1602,6 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, */ private Map keywordsCreatorByBidType( ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1602,21 +1617,21 @@ private Map keywordsCreatorByBidType( final boolean isBannerNull = banner == null || banner.isNull(); if (!isBannerNull) { result.put( - BidType.banner, createKeywordsCreator(targeting, isApp, banner, bidRequest, account, bidWarnings)); + BidType.banner, createKeywordsCreator(targeting, banner, bidRequest, account, bidWarnings)); } final ObjectNode video = mediaTypePriceGranularity.getVideo(); final boolean isVideoNull = video == null || video.isNull(); if (!isVideoNull) { result.put( - BidType.video, createKeywordsCreator(targeting, isApp, video, bidRequest, account, bidWarnings)); + BidType.video, createKeywordsCreator(targeting, video, bidRequest, account, bidWarnings)); } final ObjectNode xNative = mediaTypePriceGranularity.getXNative(); final boolean isNativeNull = xNative == null || xNative.isNull(); if (!isNativeNull) { result.put( - BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, account, bidWarnings) + BidType.xNative, createKeywordsCreator(targeting, xNative, bidRequest, account, bidWarnings) ); } @@ -1624,7 +1639,6 @@ BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, ac } private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targeting, - boolean isApp, JsonNode priceGranularity, BidRequest bidRequest, Account account, @@ -1632,13 +1646,20 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe final int resolvedTruncateAttrChars = resolveTruncateAttrChars(targeting, account); final String resolveKeyPrefix = resolveAndValidateKeyPrefix( bidRequest, account, resolvedTruncateAttrChars, bidWarnings); + + final String env = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAmp) + .map(ignored -> TARGETING_ENV_AMP_VALUE) + .orElse(bidRequest.getApp() == null ? null : TARGETING_ENV_APP_VALUE); + return TargetingKeywordsCreator.create( parsePriceGranularity(priceGranularity), BooleanUtils.toBoolean(targeting.getIncludewinners()), BooleanUtils.toBoolean(targeting.getIncludebidderkeys()), BooleanUtils.toBoolean(targeting.getAlwaysincludedeals()), BooleanUtils.isTrue(targeting.getIncludeformat()), - isApp, + env, resolvedTruncateAttrChars, cacheHost, cachePath, @@ -1737,7 +1758,7 @@ private static BidResponse populateSeatNonBid(AuctionContext auctionContext, Bid } private static SeatNonBid toSeatNonBid(String bidder, BidRejectionTracker bidRejectionTracker) { - final List nonBid = bidRejectionTracker.getRejectionReasons().entrySet().stream() + final List nonBid = bidRejectionTracker.getRejectedImps().entrySet().stream() .map(entry -> NonBid.of(entry.getKey(), entry.getValue())) .toList(); diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java new file mode 100644 index 00000000000..4ae7a6e3e3e --- /dev/null +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -0,0 +1,124 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidsAdjuster { + + private final ResponseBidValidator responseBidValidator; + private final PriceFloorEnforcer priceFloorEnforcer; + private final BidAdjustmentsProcessor bidAdjustmentsProcessor; + private final DsaEnforcer dsaEnforcer; + + public BidsAdjuster(ResponseBidValidator responseBidValidator, + PriceFloorEnforcer priceFloorEnforcer, + BidAdjustmentsProcessor bidAdjustmentsProcessor, + DsaEnforcer dsaEnforcer) { + + this.responseBidValidator = Objects.requireNonNull(responseBidValidator); + this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); + this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor); + this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); + } + + public List validateAndAdjustBids(List auctionParticipations, + AuctionContext auctionContext, + BidderAliases aliases) { + + return auctionParticipations.stream() + .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) + + .map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids( + auctionParticipation, + auctionContext.getBidRequest(), + auctionContext.getBidAdjustments())) + + .map(auctionParticipation -> priceFloorEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getAccount(), + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + + .map(auctionParticipation -> dsaEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .toList(); + } + + private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, + AuctionContext auctionContext, + BidderAliases aliases) { + + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + final List errors = new ArrayList<>(seatBid.getErrors()); + final List warnings = new ArrayList<>(seatBid.getWarnings()); + + final List requestCurrencies = bidRequest.getCur(); + if (requestCurrencies.size() > 1) { + warnings.add(BidderError.badInput( + "a single currency (" + requestCurrencies.getFirst() + ") has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency.")); + } + + final List bids = seatBid.getBids(); + final List validBids = new ArrayList<>(bids.size()); + + for (final BidderBid bid : bids) { + final ValidationResult validationResult = responseBidValidator.validate( + bid, + bidderResponse.getBidder(), + auctionContext, + aliases); + + if (validationResult.hasWarnings() || validationResult.hasErrors()) { + errors.add(makeValidationBidderError(bid.getBid(), validationResult)); + } + + if (!validationResult.hasErrors()) { + validBids.add(bid); + } + } + + final BidderResponse resultBidderResponse = bidderResponse.with( + seatBid.toBuilder() + .bids(validBids) + .errors(errors) + .warnings(warnings) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { + final String validationErrors = Stream.concat( + validationResult.getErrors().stream().map(message -> "Error: " + message), + validationResult.getWarnings().stream().map(message -> "Warning: " + message)) + .collect(Collectors.joining(". ")); + + final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); + return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); + } +} diff --git a/src/main/java/org/prebid/server/auction/DsaEnforcer.java b/src/main/java/org/prebid/server/auction/DsaEnforcer.java index b9b87abd37c..369842b36c6 100644 --- a/src/main/java/org/prebid/server/auction/DsaEnforcer.java +++ b/src/main/java/org/prebid/server/auction/DsaEnforcer.java @@ -72,7 +72,7 @@ public AuctionParticipation enforce(BidRequest bidRequest, } } catch (PreBidException e) { warnings.add(BidderError.invalidBid("Bid \"%s\": %s".formatted(bid.getId(), e.getMessage()))); - rejectionTracker.reject(bid.getImpid(), BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + rejectionTracker.rejectBid(bidderBid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); updatedBidderBids.remove(bidderBid); } } diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index ed537c6746c..c4b6dfd7a55 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -2,9 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; @@ -31,7 +29,6 @@ import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -50,6 +47,7 @@ import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.HttpBidderRequester; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.bidder.model.BidderBid; @@ -57,26 +55,14 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; -import org.prebid.server.hooks.execution.model.ExecutionAction; -import org.prebid.server.hooks.execution.model.ExecutionStatus; -import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; -import org.prebid.server.hooks.execution.model.HookExecutionOutcome; -import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; -import org.prebid.server.hooks.execution.model.Stage; -import org.prebid.server.hooks.execution.model.StageExecutionOutcome; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; -import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.json.JacksonMapper; @@ -96,7 +82,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; @@ -107,37 +92,17 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; -import org.prebid.server.proto.openrtb.ext.request.TraceLevel; -import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; -import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; -import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; -import org.prebid.server.proto.openrtb.ext.response.ExtModules; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.ListUtil; +import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; import java.time.Clock; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -146,13 +111,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -/** - * Executes an OpenRTB v2.5-2.6 Auction. - */ public class ExchangeService { private static final Logger logger = LoggerFactory.getLogger(ExchangeService.class); @@ -161,8 +121,6 @@ public class ExchangeService { private static final String PREBID_EXT = "prebid"; private static final String BIDDER_EXT = "bidder"; private static final String TID_EXT = "tid"; - private static final String ORIGINAL_BID_CPM = "origbidcpm"; - private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; private static final String ALL_BIDDERS_CONFIG = "*"; private static final Integer DEFAULT_MULTIBID_LIMIT_MIN = 1; private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; @@ -174,6 +132,7 @@ public class ExchangeService { private final StoredResponseProcessor storedResponseProcessor; private final PrivacyEnforcementService privacyEnforcementService; private final FpdResolver fpdResolver; + private final ImpAdjuster impAdjuster; private final SupplyChainResolver supplyChainResolver; private final DebugResolver debugResolver; private final MediaTypeProcessor mediaTypeProcessor; @@ -182,17 +141,13 @@ public class ExchangeService { private final TimeoutFactory timeoutFactory; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final HttpBidderRequester httpBidderRequester; - private final ResponseBidValidator responseBidValidator; - private final CurrencyConversionService currencyService; private final BidResponseCreator bidResponseCreator; private final BidResponsePostProcessor bidResponsePostProcessor; private final HookStageExecutor hookStageExecutor; private final HttpInteractionLogger httpInteractionLogger; private final PriceFloorAdjuster priceFloorAdjuster; - private final PriceFloorEnforcer priceFloorEnforcer; private final PriceFloorProcessor priceFloorProcessor; - private final DsaEnforcer dsaEnforcer; - private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final BidsAdjuster bidsAdjuster; private final Metrics metrics; private final Clock clock; private final JacksonMapper mapper; @@ -204,6 +159,7 @@ public ExchangeService(double logSamplingRate, StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, MediaTypeProcessor mediaTypeProcessor, @@ -212,17 +168,13 @@ public ExchangeService(double logSamplingRate, TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, PriceFloorProcessor priceFloorProcessor, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -234,6 +186,7 @@ public ExchangeService(double logSamplingRate, this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.impAdjuster = Objects.requireNonNull(impAdjuster); this.supplyChainResolver = Objects.requireNonNull(supplyChainResolver); this.debugResolver = Objects.requireNonNull(debugResolver); this.mediaTypeProcessor = Objects.requireNonNull(mediaTypeProcessor); @@ -242,17 +195,13 @@ public ExchangeService(double logSamplingRate, this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.httpBidderRequester = Objects.requireNonNull(httpBidderRequester); - this.responseBidValidator = Objects.requireNonNull(responseBidValidator); - this.currencyService = Objects.requireNonNull(currencyService); this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator); this.bidResponsePostProcessor = Objects.requireNonNull(bidResponsePostProcessor); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); - this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); - this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); - this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.bidsAdjuster = Objects.requireNonNull(bidsAdjuster); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -263,9 +212,8 @@ public ExchangeService(double logSamplingRate, public Future holdAuction(AuctionContext context) { return processAuctionRequest(context) .compose(this::invokeResponseHooks) - .map(this::enrichWithAnalyticsTags) - .map(this::enrichWithHooksDebugInfo) - .map(this::updateHooksMetrics); + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo); } private Future processAuctionRequest(AuctionContext context) { @@ -292,14 +240,17 @@ private Future runAuction(AuctionContext receivedContext) { final Map bidderToMultiBid = bidderToMultiBids(bidRequest, debugWarnings); receivedContext.getBidRejectionTrackers().putAll(makeBidRejectionTrackers(bidRequest, aliases)); + final boolean debugEnabled = receivedContext.getDebugContext().isDebugEnabled(); + metrics.updateDebugRequestMetrics(debugEnabled); + metrics.updateAccountDebugRequestMetrics(account, debugEnabled); + return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), timeout) .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses)) .compose(storedResponseResult -> extractAuctionParticipations(receivedContext, storedResponseResult, aliases, bidderToMultiBid) - .map(receivedContext::with)) + .map(receivedContext::with)) .map(context -> updateRequestMetric(context, uidsCookie, aliases, account, requestTypeMetric)) - .compose(context -> CompositeFuture.join( context.getAuctionParticipations().stream() .map(auctionParticipation -> processAndRequestBids( @@ -317,8 +268,10 @@ private Future runAuction(AuctionContext receivedContext) { storedAuctionResponses, bidRequest.getImp(), context.getBidRejectionTrackers())) - .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings)) - .map(auctionParticipations -> validateAndAdjustBids(auctionParticipations, context, aliases)) + .map(auctionParticipations -> dropZeroNonDealBids( + auctionParticipations, debugWarnings, debugEnabled)) + .map(auctionParticipations -> + bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases)) .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases)) .map(context::with)) // produce response from bidder results @@ -327,27 +280,27 @@ private Future runAuction(AuctionContext receivedContext) { logger, bidResponse, context.getBidRequest(), - context.getDebugContext().isDebugEnabled())) + debugEnabled)) .compose(bidResponse -> bidResponsePostProcessor.postProcess( context.getHttpRequest(), uidsCookie, bidRequest, bidResponse, account)) .map(context::with)); } private BidderAliases aliases(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final Map aliases = prebid != null ? prebid.getAliases() : null; final Map aliasgvlids = prebid != null ? prebid.getAliasgvlids() : null; return BidderAliases.of(aliases, aliasgvlids, bidderCatalog); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); return prebid != null ? prebid.getTargeting() : null; } private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { final ExtRequestTargeting targeting = targeting(bidRequest); - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; if (targeting != null && cache != null) { @@ -379,17 +332,11 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { .build(); } } - return BidRequestCacheInfo.noCache(); } - private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { - final ExtRequest requestExt = bidRequest.getExt(); - return requestExt != null ? requestExt.getPrebid() : null; - } - private static Map bidderToMultiBids(BidRequest bidRequest, List debugWarnings) { - final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid extRequestPrebid = PbsUtil.extRequestPrebid(bidRequest); final Collection multiBids = extRequestPrebid != null ? CollectionUtils.emptyIfNull(extRequestPrebid.getMultibid()) : Collections.emptyList(); @@ -472,44 +419,12 @@ private Map makeBidRejectionTrackers(BidRequest bid entry -> new BidRejectionTracker(entry.getKey(), entry.getValue(), logSamplingRate))); } - /** - * Populates storedResponse parameter with stored {@link List} and returns {@link List} for which - * request to bidders should be performed. - */ private static StoredResponseResult populateStoredResponse(StoredResponseResult storedResponseResult, List storedResponse) { storedResponse.addAll(storedResponseResult.getAuctionStoredResponse()); return storedResponseResult; } - /** - * Takes an OpenRTB request and returns the OpenRTB requests sanitized for each bidder. - *

- * This will copy the {@link BidRequest} into a list of requests, where the bidRequest.imp[].ext field - * will only consist of the "prebid" field and the field for the appropriate bidder parameters. We will drop all - * extended fields beyond this context, so this will not be compatible with any other uses of the extension area - * i.e. the bidders will not see any other extension fields. If Imp extension name is alias, which is also defined - * in bidRequest.ext.prebid.aliases and valid, separate {@link BidRequest} will be created for this alias and sent - * to appropriate bidder. - * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext.prebid.bidder.rubicon and - * imp.ext.prebid.bidder.rubiconAlias and second with imp.ext.prebid.bidder.appnexus and - * imp.ext.prebid.bidder.rubicon. Three {@link BidRequest}s will be created: - * 1. {@link BidRequest} with one {@link Imp}, where bidder extension points to rubiconAlias extension and will be - * sent to Rubicon bidder. - * 2. {@link BidRequest} with two {@link Imp}s, where bidder extension points to appropriate rubicon extension from - * original {@link BidRequest} and will be sent to Rubicon bidder. - * 3. {@link BidRequest} with one {@link Imp}, where bidder extension points to appnexus extension and will be sent - * to Appnexus bidder. - *

- * Each of the created {@link BidRequest}s will have bidrequest.user.buyerid field populated with the value from - * bidrequest.user.ext.prebid.buyerids or {@link UidsCookie} corresponding to bidder's family name unless buyerid - * is already in the original OpenRTB request (in this case it will not be overridden). - * In case if bidrequest.user.ext.prebid.buyerids contains values after extracting those values it will be cleared - * in order to avoid leaking of buyerids across bidders. - *

- * NOTE: the return list will only contain entries for bidders that both have the extension field in at least one - * {@link Imp}, and are known to {@link BidderCatalog} or aliases from bidRequest.ext.prebid.aliases. - */ private Future> extractAuctionParticipations( AuctionContext context, StoredResponseResult storedResponseResult, @@ -528,7 +443,6 @@ private Future> extractAuctionParticipations( .toList(); final Map> impBidderToStoredBidResponse = storedResponseResult.getImpBidderToStoredBidResponse(); - return makeAuctionParticipation( bidders, context, @@ -549,9 +463,6 @@ private static JsonNode bidderParamsFromImpExt(ObjectNode ext) { return ext.get(PREBID_EXT).get(BIDDER_EXT); } - /** - * Checks if bidder name is valid in case when bidder can also be alias name. - */ private boolean isValidBidder(String bidder, BidderAliases aliases) { return bidderCatalog.isValidName(bidder) || aliases.isAliasDefined(bidder); } @@ -567,21 +478,6 @@ private static boolean isBidderCallActivityAllowed(String bidder, AuctionContext activityInvocationPayload); } - /** - * Splits the input request into requests which are sanitized for each bidder. Intended behavior is: - *

- * - bidrequest.imp[].ext will only contain the "prebid" field and a "bidder" field which has the params for - * the intended Bidder. - *

- * - bidrequest.user.buyeruid will be set to that Bidder's ID. - *

- * - bidrequest.ext.prebid.data.bidders will be removed. - *

- * - bidrequest.ext.prebid.bidders will be staying in corresponding bidder only. - *

- * - bidrequest.user.ext.data, bidrequest.app.ext.data, bidrequest.dooh.ext.data and bidrequest.site.ext.data - * will be removed for bidders that don't have first party data allowed. - */ private Future> makeAuctionParticipation( List bidders, AuctionContext context, @@ -634,10 +530,6 @@ private Map getBiddersToConfigs(ExtRequestPrebid pr return bidderToConfig; } - /** - * Retrieves user eids from {@link ExtRequestPrebid} and converts them to map, where keys are eids sources - * and values are allowed bidders - */ private Map> getEidPermissions(ExtRequestPrebid prebid) { final ExtRequestPrebidData prebidData = prebid != null ? prebid.getData() : null; final List eidPermissions = prebidData != null @@ -648,9 +540,6 @@ private Map> getEidPermissions(ExtRequestPrebid prebid) { ExtRequestPrebidDataEidPermissions::getBidders)); } - /** - * Extracts a list of bidders for which first party data is allowed from {@link ExtRequestPrebidData} model. - */ private static List firstPartyDataBidders(ExtRequest requestExt) { final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final ExtRequestPrebidData data = prebid == null ? null : prebid.getData(); @@ -679,13 +568,6 @@ private Map prepareUsers(List bidders, return bidderToUser; } - /** - * Returns original {@link User} if user.buyeruid already contains uid value for bidder. - * Otherwise, returns new {@link User} containing updated {@link ExtUser} and user.buyeruid. - *

- * Also, removes user.ext.prebid (if present), user.ext.data and user.data (in case bidder does not use first - * party data). - */ private User prepareUser(String bidder, AuctionContext context, BidderAliases aliases, @@ -710,7 +592,7 @@ private User prepareUser(String bidder, userBuilder.buyeruid(buyerUidUpdateResult.getValue()); if (shouldUpdateUserEids) { - userBuilder.eids(nullIfEmpty(allowedUserEids)); + userBuilder.eids(ListUtil.nullIfEmpty(allowedUserEids)); } if (shouldUpdateUserExt) { @@ -737,9 +619,6 @@ private List extractUserEids(User user) { return user != null ? user.getEids() : null; } - /** - * Returns {@link List} allowed by {@param eidPermissions} per source per bidder. - */ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions) { return CollectionUtils.emptyIfNull(userEids) .stream() @@ -747,10 +626,6 @@ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions, String bidder) { final List allowedBidders = eidPermissions.get(source); return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() @@ -758,9 +633,6 @@ private boolean isUserEidAllowed(String source, Map> eidPer || EID_ALLOWED_FOR_ALL_BIDDERS.equals(allowedBidder)); } - /** - * Returns shuffled list of {@link AuctionParticipation} with {@link BidRequest}. - */ private List getAuctionParticipation( List bidderPrivacyResults, BidRequest bidRequest, @@ -797,7 +669,7 @@ private List getAuctionParticipation( * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ private static Map bidderToPrebidBidders(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); if (bidders == null || bidders.isNull()) { @@ -813,9 +685,6 @@ private static Map bidderToPrebidBidders(BidRequest bidRequest return bidderToPrebidParameters; } - /** - * Returns {@link AuctionParticipation} for the given bidder. - */ private AuctionParticipation createAuctionParticipation( BidderPrivacyResult bidderPrivacyResult, Map> impBidderToStoredBidResponse, @@ -832,7 +701,7 @@ private AuctionParticipation createAuctionParticipation( if (blockedRequestByTcf) { context.getBidRejectionTrackers() .get(bidder) - .rejectAll(BidRejectionReason.REJECTED_BY_PRIVACY); + .rejectAllImps(BidRejectionReason.REQUEST_BLOCKED_PRIVACY); return AuctionParticipation.builder() .bidder(bidder) @@ -852,6 +721,7 @@ private AuctionParticipation createAuctionParticipation( bidderToMultiBid, biddersToConfigs, bidderToPrebidBidders, + bidderAliases, context); final BidderRequest bidderRequest = BidderRequest.builder() @@ -878,6 +748,7 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, Map bidderToMultiBid, Map biddersToConfigs, Map bidderToPrebidBidders, + BidderAliases bidderAliases, AuctionContext context) { final String bidder = bidderPrivacyResult.getRequestBidder(); @@ -938,6 +809,7 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, transmitTid, useFirstPartyData, context.getAccount(), + bidderAliases, context.getDebugWarnings()); return bidRequest.toBuilder() @@ -975,10 +847,13 @@ private List prepareImps(String bidder, boolean transmitTid, boolean useFirstPartyData, Account account, + BidderAliases bidderAliases, List debugWarnings) { return bidRequest.getImp().stream() .filter(imp -> bidderParamsFromImpExt(imp.getExt()).hasNonNull(bidder)) + .map(imp -> imp.toBuilder().ext(imp.getExt().deepCopy()).build()) + .map(imp -> impAdjuster.adjust(imp, bidder, bidderAliases, debugWarnings)) .map(imp -> prepareImp(imp, bidder, bidRequest, transmitTid, useFirstPartyData, account, debugWarnings)) .toList(); } @@ -1013,18 +888,17 @@ private ObjectNode prepareImpExt(String bidder, BigDecimal adjustedFloor, boolean transmitTid, boolean useFirstPartyData) { - - final ObjectNode modifiedImpExt = impExt.deepCopy(); + final JsonNode bidderNode = bidderParamsFromImpExt(impExt).get(bidder); final JsonNode impExtPrebid = prepareImpExt(impExt.get(PREBID_EXT), adjustedFloor); Optional.ofNullable(impExtPrebid).ifPresentOrElse( - ext -> modifiedImpExt.set(PREBID_EXT, ext), - () -> modifiedImpExt.remove(PREBID_EXT)); - modifiedImpExt.set(BIDDER_EXT, bidderParamsFromImpExt(impExt).get(bidder)); + ext -> impExt.set(PREBID_EXT, ext), + () -> impExt.remove(PREBID_EXT)); + impExt.set(BIDDER_EXT, bidderNode); if (!transmitTid) { - modifiedImpExt.remove(TID_EXT); + impExt.remove(TID_EXT); } - return fpdResolver.resolveImpExt(modifiedImpExt, useFirstPartyData); + return fpdResolver.resolveImpExt(impExt, useFirstPartyData); } private JsonNode prepareImpExt(JsonNode extImpPrebidNode, BigDecimal adjustedFloor) { @@ -1235,16 +1109,20 @@ private Future processAndRequestBids(AuctionContext auctionConte final String bidderName = bidderRequest.getBidder(); final MediaTypeProcessingResult mediaTypeProcessingResult = mediaTypeProcessor.process( bidderRequest.getBidRequest(), bidderName, aliases, auctionContext.getAccount()); - final List mediaTypeProcessingErrors = mediaTypeProcessingResult.getErrors(); if (mediaTypeProcessingResult.isRejected()) { - auctionContext.getBidRejectionTrackers() - .get(bidderName) - .rejectAll(BidRejectionReason.REJECTED_BY_MEDIA_TYPE); - final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() - .warnings(mediaTypeProcessingErrors) - .build(); - return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, + mediaTypeProcessingErrors, + bidderName); + } + if (isUnacceptableCurrency(auctionContext, aliases.resolveBidder(bidderName))) { + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, + List.of(BidderError.generic("No match between the configured currencies and bidRequest.cur")), + bidderName); } return Future.succeededFuture(mediaTypeProcessingResult.getBidRequest()) @@ -1255,6 +1133,34 @@ private Future processAndRequestBids(AuctionContext auctionConte addWarnings(bidderResponse.getSeatBid(), mediaTypeProcessingErrors))); } + private boolean isUnacceptableCurrency(AuctionContext auctionContext, String originalBidderName) { + final List requestCurrencies = auctionContext.getBidRequest().getCur(); + final List bidAcceptableCurrencies = + Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) + .map(BidderInfo::getCurrencyAccepted) + .orElse(null); + + if (CollectionUtils.isEmpty(requestCurrencies) || CollectionUtils.isEmpty(bidAcceptableCurrencies)) { + return false; + } + + return !CollectionUtils.containsAny(requestCurrencies, bidAcceptableCurrencies); + } + + private static Future processReject(AuctionContext auctionContext, + BidRejectionReason bidRejectionReason, + List warnings, + String bidderName) { + + auctionContext.getBidRejectionTrackers() + .get(bidderName) + .rejectAllImps(bidRejectionReason); + final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() + .warnings(warnings) + .build(); + return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + } + private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { return CollectionUtils.isNotEmpty(warnings) ? seatBid.toBuilder() @@ -1287,7 +1193,7 @@ private Future requestBidsOrRejectBidder( if (hookStageResult.isShouldReject()) { auctionContext.getBidRejectionTrackers() .get(bidderRequest.getBidder()) - .rejectAll(BidRejectionReason.REJECTED_BY_HOOK); + .rejectAllImps(BidRejectionReason.REQUEST_BLOCKED_GENERAL); return Future.succeededFuture(BidderResponse.of(bidderRequest.getBidder(), BidderSeatBid.empty(), 0)); } @@ -1359,15 +1265,18 @@ private BidderResponse rejectBidderResponseOrProceed(HookStageExecutionResult dropZeroNonDealBids(List auctionParticipations, - List debugWarnings) { + List debugWarnings, + boolean isDebugEnabled) { return auctionParticipations.stream() - .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings)) + .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings, isDebugEnabled)) .toList(); } private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionParticipation, - List debugWarnings) { + List debugWarnings, + boolean isDebugEnabled) { + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); final List bidderBids = seatBid.getBids(); @@ -1377,8 +1286,11 @@ private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionPar final Bid bid = bidderBid.getBid(); if (isZeroNonDealBids(bid.getPrice(), bid.getDealid())) { metrics.updateAdapterRequestErrorMetric(bidderResponse.getBidder(), MetricName.unknown_error); - debugWarnings.add("Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'" - .formatted(bid.getId())); + if (isDebugEnabled) { + debugWarnings.add( + "Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'" + .formatted(bid.getId())); + } } else { validBids.add(bidderBid); } @@ -1395,210 +1307,10 @@ private boolean isZeroNonDealBids(BigDecimal price, String dealId) { || (price.compareTo(BigDecimal.ZERO) == 0 && StringUtils.isBlank(dealId)); } - private List validateAndAdjustBids(List auctionParticipations, - AuctionContext auctionContext, - BidderAliases aliases) { - - return auctionParticipations.stream() - .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) - .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) - .map(auctionParticipation -> priceFloorEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getAccount(), - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .map(auctionParticipation -> dsaEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .toList(); - } - - /** - * Validates bid response from exchange. - *

- * Removes invalid bids from response and adds corresponding error to {@link BidderSeatBid}. - *

- * Returns input argument as the result if no errors found or creates new {@link BidderResponse} otherwise. - */ - private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, - AuctionContext auctionContext, - BidderAliases aliases) { - - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidRequest bidRequest = auctionContext.getBidRequest(); - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final List errors = new ArrayList<>(seatBid.getErrors()); - final List warnings = new ArrayList<>(seatBid.getWarnings()); - - final List requestCurrencies = bidRequest.getCur(); - if (requestCurrencies.size() > 1) { - errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" - .formatted(requestCurrencies.getFirst()))); - } - - final List bids = seatBid.getBids(); - final List validBids = new ArrayList<>(bids.size()); - - for (final BidderBid bid : bids) { - final ValidationResult validationResult = responseBidValidator.validate( - bid, - bidderResponse.getBidder(), - auctionContext, - aliases); - - if (validationResult.hasWarnings() || validationResult.hasErrors()) { - errors.add(makeValidationBidderError(bid.getBid(), validationResult)); - } - - if (!validationResult.hasErrors()) { - validBids.add(bid); - } - } - - final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() - ? bidderResponse - : bidderResponse.with( - seatBid.toBuilder() - .bids(validBids) - .errors(errors) - .warnings(warnings) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { - final String validationErrors = Stream.concat( - validationResult.getErrors().stream().map(message -> "Error: " + message), - validationResult.getWarnings().stream().map(message -> "Warning: " + message)) - .collect(Collectors.joining(". ")); - - final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); - return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); - } - - /** - * Performs changes on {@link Bid}s price depends on different between adServerCurrency and bidCurrency, - * and adjustment factor. Will drop bid if currency conversion is needed but not possible. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ - private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, - BidRequest bidRequest) { - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - - final List bidderBids = seatBid.getBids(); - if (bidderBids.isEmpty()) { - return auctionParticipation; - } - - final List updatedBidderBids = new ArrayList<>(bidderBids.size()); - final List errors = new ArrayList<>(seatBid.getErrors()); - final String adServerCurrency = bidRequest.getCur().getFirst(); - - for (final BidderBid bidderBid : bidderBids) { - try { - final BidderBid updatedBidderBid = - updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); - updatedBidderBids.add(updatedBidderBid); - } catch (PreBidException e) { - errors.add(BidderError.generic(e.getMessage())); - } - } - - final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() - .bids(updatedBidderBids) - .errors(errors) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, - BidderResponse bidderResponse, - BidRequest bidRequest, - String adServerCurrency) { - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidderBid.getBidCurrency(); - final BigDecimal price = bid.getPrice(); - - final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); - - final BigDecimal priceAdjustmentFactor = - bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); - final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); - - final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); - - updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); - - final Bid.BidBuilder bidBuilder = bid.toBuilder(); - if (adjustedPrice.compareTo(price) != 0) { - bidBuilder.price(adjustedPrice); - } - - if (!updatedBidExt.isEmpty()) { - bidBuilder.ext(updatedBidExt); - } - - return bidderBid.toBuilder().bid(bidBuilder.build()).build(); - } - - private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { - final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); - - return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); - } - - private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - return prebid != null ? prebid.getBidadjustmentfactors() : null; - } - - private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { - return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 - ? price.multiply(priceAdjustmentFactor) - : price; - } - - private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); - if (StringUtils.isNotBlank(bidCurrency)) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); - } - } - - private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { - node.set(propertyName, propertyValue); - } - private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } - /** - * Updates 'request_time', 'responseTime', 'timeout_request', 'error_requests', 'no_bid_requests', - * 'prices' metrics for each {@link AuctionParticipation}. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ private List updateResponsesMetrics(List auctionParticipations, Account account, BidderAliases aliases) { @@ -1647,9 +1359,6 @@ private Future invokeResponseHooks(AuctionContext auctionContext .map(auctionContext::with); } - /** - * Resolves {@link MetricName} by {@link BidderError.Type} value. - */ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { return switch (errorType) { case bad_input -> MetricName.badinput; @@ -1657,365 +1366,7 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { case failed_to_request_bids -> MetricName.failedtorequestbids; case timeout -> MetricName.timeout; case invalid_bid -> MetricName.bid_validation; - case rejected_ipf, generic, invalid_creative -> MetricName.unknown_error; + case rejected_ipf, generic -> MetricName.unknown_error; }; } - - private AuctionContext enrichWithAnalyticsTags(AuctionContext context) { - final boolean clientDetailsEnabled = isClientDetailsEnabled(context); - if (!clientDetailsEnabled) { - return context; - } - - final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) - .map(Account::getAnalytics) - .map(AccountAnalyticsConfig::isAllowClientDetails) - .orElse(false); - - if (!allowClientDetails) { - return addClientDetailsWarning(context); - } - - final List extAnalyticsTags = toExtAnalyticsTags(context); - - if (extAnalyticsTags == null) { - return context; - } - - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); - - final ExtBidResponsePrebid updatedExtPrebid = extPrebid - .map(ExtBidResponsePrebid::toBuilder) - .orElse(ExtBidResponsePrebid.builder()) - .analytics(ExtAnalytics.of(extAnalyticsTags)) - .build(); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .prebid(updatedExtPrebid) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private static boolean isClientDetailsEnabled(AuctionContext context) { - final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) - .map(BidRequest::getExt) - .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getAnalytics) - .orElse(null); - - if (notObjectNode(analytics)) { - return false; - } - - final JsonNode options = analytics.get("options"); - if (notObjectNode(options)) { - return false; - } - - final JsonNode enableClientDetails = options.get("enableclientdetails"); - return enableClientDetails != null - && enableClientDetails.isBoolean() - && enableClientDetails.asBoolean(); - } - - private static boolean notObjectNode(JsonNode jsonNode) { - return jsonNode == null || !jsonNode.isObject(); - } - - private static AuctionContext addClientDetailsWarning(AuctionContext context) { - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - - final Map> warnings = ext - .map(ExtBidResponse::getWarnings) - .orElse(Collections.emptyMap()); - final List prebidWarnings = ObjectUtils.defaultIfNull( - warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), - Collections.emptyList()); - - final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); - updatedPrebidWarnings.add(ExtBidderError.of( - BidderError.Type.generic.getCode(), - "analytics.options.enableclientdetails not enabled for account")); - final Map> updatedWarnings = new HashMap<>(warnings); - updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .warnings(updatedWarnings) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private AuctionContext enrichWithHooksDebugInfo(AuctionContext context) { - final ExtModules extModules = toExtModules(context); - - if (extModules == null) { - return context; - } - - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); - - final ExtBidResponsePrebid updatedExtPrebid = extPrebid - .map(ExtBidResponsePrebid::toBuilder) - .orElse(ExtBidResponsePrebid.builder()) - .modules(extModules) - .build(); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .prebid(updatedExtPrebid) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private static ExtModules toExtModules(AuctionContext context) { - final Map>> errors = - toHookMessages(context, HookExecutionOutcome::getErrors); - final Map>> warnings = - toHookMessages(context, HookExecutionOutcome::getWarnings); - final ExtModulesTrace trace = toHookTrace(context); - return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; - } - - private static Map>> toHookMessages( - AuctionContext context, - Function> messagesGetter) { - - if (!context.getDebugContext().isDebugEnabled()) { - return null; - } - - final Map> hookOutcomesByModule = - context.getHookExecutionContext().getStageOutcomes().values().stream() - .flatMap(Collection::stream) - .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) - .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) - .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) - .collect(Collectors.groupingBy( - hookOutcome -> hookOutcome.getHookId().getModuleCode())); - - final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - outcomes -> outcomes.getValue().stream() - .collect(Collectors.groupingBy( - hookOutcome -> hookOutcome.getHookId().getHookImplCode())) - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - messagesLists -> messagesLists.getValue().stream() - .map(messagesGetter) - .flatMap(Collection::stream) - .toList())))); - - return !messagesByModule.isEmpty() ? messagesByModule : null; - } - - private static ExtModulesTrace toHookTrace(AuctionContext context) { - final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); - - if (traceLevel == null) { - return null; - } - - final List stages = context.getHookExecutionContext().getStageOutcomes() - .entrySet().stream() - .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) - .filter(Objects::nonNull) - .toList(); - - if (stages.isEmpty()) { - return null; - } - - final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); - return ExtModulesTrace.of(executionTime, stages); - } - - private static ExtModulesTraceStage toTraceStage(Stage stage, - List stageOutcomes, - TraceLevel level) { - - final List extStageOutcomes = stageOutcomes.stream() - .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) - .filter(Objects::nonNull) - .toList(); - - if (extStageOutcomes.isEmpty()) { - return null; - } - - final long executionTime = extStageOutcomes.stream() - .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) - .max() - .orElse(0L); - - return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); - } - - private static ExtModulesTraceStageOutcome toTraceStageOutcome( - StageExecutionOutcome stageOutcome, TraceLevel level) { - - final List groups = stageOutcome.getGroups().stream() - .map(group -> toTraceGroup(group, level)) - .toList(); - - if (groups.isEmpty()) { - return null; - } - - final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); - return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); - } - - private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { - final List invocationResults = group.getHooks().stream() - .map(hook -> toTraceInvocationResult(hook, level)) - .toList(); - - final long executionTime = invocationResults.stream() - .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) - .max() - .orElse(0L); - - return ExtModulesTraceGroup.of(executionTime, invocationResults); - } - - private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, - TraceLevel level) { - return ExtModulesTraceInvocationResult.builder() - .hookId(hook.getHookId()) - .executionTime(hook.getExecutionTime()) - .status(hook.getStatus()) - .message(hook.getMessage()) - .action(hook.getAction()) - .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) - .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) - .build(); - } - - private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { - if (analyticsTags == null) { - return null; - } - - return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() - .filter(Objects::nonNull) - .map(ExchangeService::toTraceAnalyticsActivity) - .toList()); - } - - private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity( - org.prebid.server.hooks.v1.analytics.Activity activity) { - - return ExtModulesTraceAnalyticsActivity.of( - activity.name(), - activity.status(), - CollectionUtils.emptyIfNull(activity.results()).stream() - .filter(Objects::nonNull) - .map(ExchangeService::toTraceAnalyticsResult) - .toList()); - } - - private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { - final AppliedTo appliedTo = result.appliedTo(); - final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null - ? ExtModulesTraceAnalyticsAppliedTo.builder() - .impIds(appliedTo.impIds()) - .bidders(appliedTo.bidders()) - .request(appliedTo.request() ? Boolean.TRUE : null) - .response(appliedTo.response() ? Boolean.TRUE : null) - .bidIds(appliedTo.bidIds()) - .build() - : null; - - return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); - } - - private static List toExtAnalyticsTags(AuctionContext context) { - return context.getHookExecutionContext().getStageOutcomes().entrySet().stream() - .flatMap(stageToExecutionOutcome -> stageToExecutionOutcome.getValue().stream() - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .filter(hookExecutionOutcome -> hookExecutionOutcome.getAnalyticsTags() != null) - .map(hookExecutionOutcome -> ExtAnalyticsTags.of( - stageToExecutionOutcome.getKey(), - hookExecutionOutcome.getHookId().getModuleCode(), - toTraceAnalyticsTags(hookExecutionOutcome.getAnalyticsTags())))) - .toList(); - } - - private AuctionContext updateHooksMetrics(AuctionContext context) { - final EnumMap> stageOutcomes = - context.getHookExecutionContext().getStageOutcomes(); - - final Account account = context.getAccount(); - - stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); - - // account might be null if request is rejected by the entrypoint hook - if (account != null) { - stageOutcomes.values().stream() - .flatMap(Collection::stream) - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .collect(Collectors.groupingBy( - outcome -> outcome.getHookId().getModuleCode(), - Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) - .forEach((moduleCode, executionTime) -> - metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime)); - } - - return context; - } - - private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { - stageOutcomes.stream() - .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) - .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) - .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); - } - - private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { - final HookId hookId = hookOutcome.getHookId(); - final ExecutionStatus status = hookOutcome.getStatus(); - final ExecutionAction action = hookOutcome.getAction(); - final String moduleCode = hookId.getModuleCode(); - - metrics.updateHooksMetrics( - moduleCode, - stage, - hookId.getHookImplCode(), - status, - hookOutcome.getExecutionTime(), - action); - - // account might be null if request is rejected by the entrypoint hook - if (account != null) { - metrics.updateAccountHooksMetrics(account, moduleCode, status, action); - } - } - - private List nullIfEmpty(List value) { - return CollectionUtils.isEmpty(value) ? null : value; - } } diff --git a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java index 4c1a3b0b8cf..609e7481b81 100644 --- a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java +++ b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java new file mode 100644 index 00000000000..ad8cd86410c --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java @@ -0,0 +1,247 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class HookDebugInfoEnricher { + + private HookDebugInfoEnricher() { + } + + public static AuctionContext enrichWithHooksDebugInfo(AuctionContext context) { + final ExtModules extModules = toExtModules(context); + + if (extModules == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .modules(extModules) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static ExtModules toExtModules(AuctionContext context) { + final Map>> errors = + toHookMessages(context, HookExecutionOutcome::getErrors); + final Map>> warnings = + toHookMessages(context, HookExecutionOutcome::getWarnings); + final ExtModulesTrace trace = toHookTrace(context); + return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; + } + + private static Map>> toHookMessages( + AuctionContext context, + Function> messagesGetter) { + + if (!context.getDebugContext().isDebugEnabled()) { + return null; + } + + final Map> hookOutcomesByModule = + context.getHookExecutionContext().getStageOutcomes().values().stream() + .flatMap(Collection::stream) + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getModuleCode())); + + final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + outcomes -> outcomes.getValue().stream() + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getHookImplCode())) + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + messagesLists -> messagesLists.getValue().stream() + .map(messagesGetter) + .flatMap(Collection::stream) + .toList())))); + + return !messagesByModule.isEmpty() ? messagesByModule : null; + } + + private static ExtModulesTrace toHookTrace(AuctionContext context) { + final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); + + if (traceLevel == null) { + return null; + } + + final List stages = context.getHookExecutionContext().getStageOutcomes() + .entrySet().stream() + .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) + .filter(Objects::nonNull) + .toList(); + + if (stages.isEmpty()) { + return null; + } + + final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); + return ExtModulesTrace.of(executionTime, stages); + } + + private static ExtModulesTraceStage toTraceStage(Stage stage, + List stageOutcomes, + TraceLevel level) { + + final List extStageOutcomes = stageOutcomes.stream() + .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) + .filter(Objects::nonNull) + .toList(); + + if (extStageOutcomes.isEmpty()) { + return null; + } + + final long executionTime = extStageOutcomes.stream() + .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); + } + + private static ExtModulesTraceStageOutcome toTraceStageOutcome( + StageExecutionOutcome stageOutcome, TraceLevel level) { + + final List groups = stageOutcome.getGroups().stream() + .map(group -> toTraceGroup(group, level)) + .toList(); + + if (groups.isEmpty()) { + return null; + } + + final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); + return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); + } + + private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { + final List invocationResults = group.getHooks().stream() + .map(hook -> toTraceInvocationResult(hook, level)) + .toList(); + + final long executionTime = invocationResults.stream() + .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceGroup.of(executionTime, invocationResults); + } + + private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, + TraceLevel level) { + return ExtModulesTraceInvocationResult.builder() + .hookId(hook.getHookId()) + .executionTime(hook.getExecutionTime()) + .status(hook.getStatus()) + .message(hook.getMessage()) + .action(hook.getAction()) + .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) + .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) + .build(); + } + + private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { + if (analyticsTags == null) { + return null; + } + + return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() + .filter(Objects::nonNull) + .map(HookDebugInfoEnricher::toTraceAnalyticsActivity) + .toList()); + } + + private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity( + org.prebid.server.hooks.v1.analytics.Activity activity) { + + return ExtModulesTraceAnalyticsActivity.of( + activity.name(), + activity.status(), + CollectionUtils.emptyIfNull(activity.results()).stream() + .filter(Objects::nonNull) + .map(HookDebugInfoEnricher::toTraceAnalyticsResult) + .toList()); + } + + private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { + final AppliedTo appliedTo = result.appliedTo(); + final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null + ? ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(appliedTo.impIds()) + .bidders(appliedTo.bidders()) + .request(appliedTo.request() ? Boolean.TRUE : null) + .response(appliedTo.response() ? Boolean.TRUE : null) + .bidIds(appliedTo.bidIds()) + .build() + : null; + + return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); + } + + public static List toExtAnalyticsTags(AuctionContext context) { + return context.getHookExecutionContext().getStageOutcomes().entrySet().stream() + .flatMap(stageToExecutionOutcome -> stageToExecutionOutcome.getValue().stream() + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookExecutionOutcome -> hookExecutionOutcome.getAnalyticsTags() != null) + .map(hookExecutionOutcome -> ExtAnalyticsTags.of( + stageToExecutionOutcome.getKey(), + hookExecutionOutcome.getHookId().getModuleCode(), + toTraceAnalyticsTags(hookExecutionOutcome.getAnalyticsTags())))) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/auction/HooksMetricsService.java b/src/main/java/org/prebid/server/auction/HooksMetricsService.java new file mode 100644 index 00000000000..0b31d28444f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HooksMetricsService.java @@ -0,0 +1,81 @@ +package org.prebid.server.auction; + +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.model.Account; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class HooksMetricsService { + + private final Metrics metrics; + + public HooksMetricsService(Metrics metrics) { + this.metrics = Objects.requireNonNull(metrics); + } + + public AuctionContext updateHooksMetrics(AuctionContext context) { + final EnumMap> stageOutcomes = + context.getHookExecutionContext().getStageOutcomes(); + + final Account account = context.getAccount(); + + stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + stageOutcomes.values().stream() + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookOutcome -> hookOutcome.getAction() != ExecutionAction.no_invocation) + .collect(Collectors.groupingBy( + outcome -> outcome.getHookId().getModuleCode(), + Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) + .forEach((moduleCode, executionTime) -> + metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime)); + } + + return context; + } + + private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { + stageOutcomes.stream() + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); + } + + private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { + final HookId hookId = hookOutcome.getHookId(); + final ExecutionStatus status = hookOutcome.getStatus(); + final ExecutionAction action = hookOutcome.getAction(); + final String moduleCode = hookId.getModuleCode(); + + metrics.updateHooksMetrics( + moduleCode, + stage, + hookId.getHookImplCode(), + status, + hookOutcome.getExecutionTime(), + action); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + metrics.updateAccountHooksMetrics(account, moduleCode, status, action); + } + } +} diff --git a/src/main/java/org/prebid/server/auction/ImpAdjuster.java b/src/main/java/org/prebid/server/auction/ImpAdjuster.java new file mode 100644 index 00000000000..86e581b06e8 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/ImpAdjuster.java @@ -0,0 +1,98 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.validation.ImpValidator; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class ImpAdjuster { + + private static final String IMP_EXT = "ext"; + private static final String EXT_PREBID = "prebid"; + private static final String EXT_PREBID_BIDDER = "bidder"; + private static final String EXT_PREBID_IMP = "imp"; + + private final ImpValidator impValidator; + private final JacksonMapper jacksonMapper; + private final JsonMerger jsonMerger; + + public ImpAdjuster(JacksonMapper jacksonMapper, + JsonMerger jsonMerger, + ImpValidator impValidator) { + + this.impValidator = Objects.requireNonNull(impValidator); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public Imp adjust(Imp originalImp, String bidder, BidderAliases bidderAliases, List debugMessages) { + final JsonNode impExtPrebidImp = bidderParamsFromImpExtPrebidImp(originalImp.getExt()); + if (impExtPrebidImp == null) { + return originalImp; + } + + final JsonNode bidderNode = getBidderNode(bidder, bidderAliases, impExtPrebidImp); + + if (bidderNode == null || bidderNode.isEmpty()) { + removeImpExtPrebidImp(originalImp.getExt()); + return originalImp; + } + + removeExtPrebidBidder(bidderNode); + + try { + final JsonNode originalImpNode = jacksonMapper.mapper().valueToTree(originalImp); + final JsonNode mergedImpNode = jsonMerger.merge(bidderNode, originalImpNode); + + removeImpExtPrebidImp(mergedImpNode.get(IMP_EXT)); + + final Imp resultImp = jacksonMapper.mapper().convertValue(mergedImpNode, Imp.class); + + impValidator.validateImp(resultImp); + return resultImp; + } catch (Exception e) { + debugMessages.add("imp.ext.prebid.imp.%s can not be merged into original imp [id=%s], reason: %s" + .formatted(bidder, originalImp.getId(), e.getMessage())); + removeImpExtPrebidImp(originalImp.getExt()); + return originalImp; + } + } + + private static JsonNode bidderParamsFromImpExtPrebidImp(ObjectNode ext) { + return Optional.ofNullable(ext) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(prebidNode -> prebidNode.get(EXT_PREBID_IMP)) + .orElse(null); + } + + private static JsonNode getBidderNode(String bidderName, BidderAliases bidderAliases, JsonNode node) { + final Iterator fieldNames = node.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (bidderAliases.isSame(fieldName, bidderName)) { + return node.get(fieldName); + } + } + return null; + } + + private static void removeExtPrebidBidder(JsonNode bidderNode) { + Optional.ofNullable(bidderNode.get(IMP_EXT)) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .ifPresent(ext -> ext.remove(EXT_PREBID_BIDDER)); + } + + private static void removeImpExtPrebidImp(JsonNode impExt) { + Optional.ofNullable(impExt.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .ifPresent(prebid -> prebid.remove(EXT_PREBID_IMP)); + } +} diff --git a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java index 3256ed360e1..964b89b8b3e 100644 --- a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java +++ b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java @@ -31,12 +31,14 @@ private static ImpMediaType resolveBidAdjustmentVideoMediaType(String bidImpId, .orElse(null); if (bidImpVideo == null) { - return null; + return ImpMediaType.video_outstream; } final Integer placement = bidImpVideo.getPlacement(); - return placement == null || Objects.equals(placement, 1) - ? ImpMediaType.video + final Integer plcmt = bidImpVideo.getPlcmt(); + + return Objects.equals(placement, 1) || Objects.equals(plcmt, 1) + ? ImpMediaType.video_instream : ImpMediaType.video_outstream; } } diff --git a/src/main/java/org/prebid/server/auction/PriceGranularity.java b/src/main/java/org/prebid/server/auction/PriceGranularity.java index 81cec03fd62..75620e4d953 100644 --- a/src/main/java/org/prebid/server/auction/PriceGranularity.java +++ b/src/main/java/org/prebid/server/auction/PriceGranularity.java @@ -72,6 +72,12 @@ public static PriceGranularity createFromString(String stringPriceGranularity) { } } + public static PriceGranularity createFromStringOrDefault(String stringPriceGranularity) { + return isValidStringPriceGranularityType(stringPriceGranularity) + ? STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.valueOf(stringPriceGranularity)) + : PriceGranularity.DEFAULT; + } + /** * Returns list of {@link ExtGranularityRange}s. */ diff --git a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java index dd8c95c0f50..e833b317cc2 100644 --- a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java +++ b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java index f982870c049..3729c5da661 100644 --- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java @@ -12,8 +12,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.InvalidStoredImpException; import org.prebid.server.exception.InvalidStoredRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java index 64ec4869a9b..1f5b0d83258 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java @@ -21,7 +21,7 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -69,31 +69,30 @@ public StoredResponseProcessor(ApplicationSettings applicationSettings, Future getStoredResponseResult(List imps, Timeout timeout) { final Map impExtPrebids = getImpsExtPrebid(imps); - final Map auctionStoredResponseToImpId = getAuctionStoredResponses(impExtPrebids); - final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, auctionStoredResponseToImpId); + final Map impIdsToStoredResponses = getAuctionStoredResponses(impExtPrebids); + final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, impIdsToStoredResponses); - final Map> impToBidderToStoredBidResponseId = getStoredBidResponses(impExtPrebids, - requiredRequestImps); + final Map> impToBidderToStoredBidResponseId = + getStoredBidResponses(impExtPrebids, requiredRequestImps); - final Set storedIds = new HashSet<>(auctionStoredResponseToImpId.keySet()); + final Set storedResponses = new HashSet<>(impIdsToStoredResponses.values()); - storedIds.addAll( - impToBidderToStoredBidResponseId.values().stream() - .flatMap(bidderToId -> bidderToId.values().stream()) - .collect(Collectors.toSet())); + impToBidderToStoredBidResponseId.values() + .forEach(bidderToStoredResponse -> storedResponses.addAll(bidderToStoredResponse.values())); - if (storedIds.isEmpty()) { - return Future.succeededFuture(StoredResponseResult.of(imps, Collections.emptyList(), - Collections.emptyMap())); + if (storedResponses.isEmpty()) { + return Future.succeededFuture( + StoredResponseResult.of(imps, Collections.emptyList(), Collections.emptyMap())); } - return applicationSettings.getStoredResponses(storedIds, timeout) + return getStoredResponses(storedResponses, timeout) .recover(exception -> Future.failedFuture(new InvalidRequestException( "Stored response fetching failed with reason: " + exception.getMessage()))) .map(storedResponseDataResult -> StoredResponseResult.of( requiredRequestImps, - convertToSeatBid(storedResponseDataResult, auctionStoredResponseToImpId), - mapStoredBidResponseIdsToValues(storedResponseDataResult.getIdToStoredResponses(), + convertToSeatBid(storedResponseDataResult, impIdsToStoredResponses), + mapStoredBidResponseIdsToValues( + storedResponseDataResult.getIdToStoredResponses(), impToBidderToStoredBidResponseId))); } @@ -107,161 +106,95 @@ Future getStoredResponseResult(String storedId, Timeout ti Collections.emptyMap())); } - private List excludeStoredAuctionResponseImps(List imps, - Map auctionStoredResponseToImpId) { - + private Map getImpsExtPrebid(List imps) { return imps.stream() - .filter(imp -> !auctionStoredResponseToImpId.containsValue(imp.getId())) - .toList(); - } - - public List updateStoredBidResponse(List auctionParticipations) { - return auctionParticipations.stream() - .map(StoredResponseProcessor::updateStoredBidResponse) - .collect(Collectors.toList()); + .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); } - private static AuctionParticipation updateStoredBidResponse(AuctionParticipation auctionParticipation) { - final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); - final BidRequest bidRequest = bidderRequest.getBidRequest(); - - final List imps = bidRequest.getImp(); - // ะor now, Stored Bid Response works only for bid requests with single imp - if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) { - return auctionParticipation; + private ExtImp getExtImp(ObjectNode extImpNode, String impId) { + try { + return mapper.mapper().treeToValue(extImpNode, ExtImp.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException( + "Error decoding bidRequest.imp.ext for impId = %s : %s".formatted(impId, e.getMessage())); } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid initialSeatBid = bidderResponse.getSeatBid(); - final BidderSeatBid adjustedSeatBid = updateSeatBid(initialSeatBid, imps.getFirst().getId()); - - return auctionParticipation.with(bidderResponse.with(adjustedSeatBid)); - } - - private static BidderSeatBid updateSeatBid(BidderSeatBid bidderSeatBid, String impId) { - final List bids = bidderSeatBid.getBids().stream() - .map(bidderBid -> resolveBidImpId(bidderBid, impId)) - .collect(Collectors.toList()); - - return bidderSeatBid.with(bids); } - private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { - final Bid bid = bidderBid.getBid(); - final String bidImpId = bid.getImpid(); - if (!StringUtils.contains(bidImpId, PBS_IMPID_MACRO)) { - return bidderBid; - } - - return bidderBid.toBuilder() - .bid(bid.toBuilder().impid(bidImpId.replace(PBS_IMPID_MACRO, impId)).build()) - .build(); + private Map getAuctionStoredResponses(Map extImpPrebids) { + return extImpPrebids.entrySet().stream() + .map(impIdToExtPrebid -> Tuple2.of( + impIdToExtPrebid.getKey(), + extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) + .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) + .collect(Collectors.toMap(Tuple2::getLeft, Tuple2::getRight)); } - List mergeWithBidderResponses(List auctionParticipations, - List storedAuctionResponses, - List imps, - Map bidRejectionTrackers) { - if (CollectionUtils.isEmpty(storedAuctionResponses)) { - return auctionParticipations; - } - - final Map bidderToAuctionParticipation = auctionParticipations.stream() - .collect(Collectors.toMap(AuctionParticipation::getBidder, Function.identity())); - final Map bidderToSeatBid = storedAuctionResponses.stream() - .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); - final Map impIdToBidType = imps.stream() - .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); - final Set responseBidders = new HashSet<>(bidderToAuctionParticipation.keySet()); - responseBidders.addAll(bidderToSeatBid.keySet()); - - return responseBidders.stream() - .map(bidder -> updateBidderResponse(bidderToAuctionParticipation.get(bidder), - bidderToSeatBid.get(bidder), impIdToBidType)) - .map(auctionParticipation -> restoreStoredBidsFromRejection(bidRejectionTrackers, auctionParticipation)) - .toList(); + private StoredResponse extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { + final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); + return Optional.ofNullable(storedAuctionResponse) + .map(ExtStoredAuctionResponse::getSeatBid) + .map(StoredResponse.StoredResponseObject::new) + .or(() -> Optional.ofNullable(storedAuctionResponse) + .map(ExtStoredAuctionResponse::getId) + .map(StoredResponse.StoredResponseId::new)) + .orElse(null); } - private static AuctionParticipation restoreStoredBidsFromRejection( - Map bidRejectionTrackers, - AuctionParticipation auctionParticipation) { - - final BidRejectionTracker bidRejectionTracker = bidRejectionTrackers.get(auctionParticipation.getBidder()); - - if (bidRejectionTracker != null) { - Optional.ofNullable(auctionParticipation.getBidderResponse()) - .map(BidderResponse::getSeatBid) - .map(BidderSeatBid::getBids) - .ifPresent(bidRejectionTracker::restoreFromRejection); - } - - return auctionParticipation; - } + private List excludeStoredAuctionResponseImps(List imps, + Map impIdToStoredResponse) { - private Map getImpsExtPrebid(List imps) { return imps.stream() - .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); - } - - private Map getAuctionStoredResponses(Map extImpPrebids) { - return extImpPrebids.entrySet().stream() - .map(impIdToExtPrebid -> Tuple2.of(impIdToExtPrebid.getKey(), - extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) - .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) - .collect(Collectors.toMap(Tuple2::getRight, Tuple2::getLeft)); + .filter(imp -> !impIdToStoredResponse.containsKey(imp.getId())) + .toList(); } - private String extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { - final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); - return storedAuctionResponse != null ? storedAuctionResponse.getId() : null; - } + private Map> getStoredBidResponses( + Map extImpPrebids, + List imps) { - private Map> getStoredBidResponses(Map extImpPrebids, - List imps) { // PBS supports stored bid response only for requests with single impression, but it can be changed in future if (imps.size() != 1) { return Collections.emptyMap(); } - final Set impsIds = imps.stream().map(Imp::getId).collect(Collectors.toSet()); - return extImpPrebids.entrySet().stream() - .filter(impIdToExtPrebid -> impsIds.contains(impIdToExtPrebid.getKey())) - .filter(impIdToExtPrebid -> CollectionUtils - .isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) - .collect(Collectors.toMap(Map.Entry::getKey, + .filter(impIdToExtPrebid -> + CollectionUtils.isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) + .collect(Collectors.toMap( + Map.Entry::getKey, impIdToStoredResponses -> resolveStoredBidResponse(impIdToStoredResponses.getValue().getStoredBidResponse()))); } - private ExtImp getExtImp(ObjectNode extImpNode, String impId) { - try { - return mapper.mapper().treeToValue(extImpNode, ExtImp.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException( - "Error decoding bidRequest.imp.ext for impId = %s : %s".formatted(impId, e.getMessage())); - } - } + private Map resolveStoredBidResponse( + List storedBidResponse) { - private Map resolveStoredBidResponse(List storedBidResponse) { return storedBidResponse.stream() - .collect(Collectors.toMap(ExtStoredBidResponse::getBidder, ExtStoredBidResponse::getId)); + .collect(Collectors.toMap( + ExtStoredBidResponse::getBidder, + extStoredBidResponse -> new StoredResponse.StoredResponseId(extStoredBidResponse.getId()))); + } + + private Future getStoredResponses(Set storedResponses, Timeout timeout) { + return applicationSettings.getStoredResponses( + storedResponses.stream() + .filter(StoredResponse.StoredResponseId.class::isInstance) + .map(StoredResponse.StoredResponseId.class::cast) + .map(StoredResponse.StoredResponseId::id) + .collect(Collectors.toSet()), + timeout); } private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult, - Map auctionStoredResponses) { + Map impIdsToStoredResponses) { + final List resolvedSeatBids = new ArrayList<>(); final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); - for (final Map.Entry storedIdToImpId : auctionStoredResponses.entrySet()) { - final String id = storedIdToImpId.getKey(); - final String impId = storedIdToImpId.getValue(); - final String rowSeatBid = idToStoredResponses.get(id); - if (rowSeatBid == null) { - throw new InvalidRequestException( - "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." - .formatted(impId, id)); - } - final List seatBids = parseSeatBid(id, rowSeatBid); + for (Map.Entry impIdToStoredResponse : impIdsToStoredResponses.entrySet()) { + final String impId = impIdToStoredResponse.getKey(); + final StoredResponse storedResponse = impIdToStoredResponse.getValue(); + final List seatBids = resolveSeatBids(storedResponse, idToStoredResponses, impId); + validateStoredSeatBid(seatBids); resolvedSeatBids.addAll(seatBids.stream() .map(seatBid -> updateSeatBidBids(seatBid, impId)) @@ -273,7 +206,7 @@ private List convertToSeatBid(StoredResponseDataResult storedResponseDa private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult) { final List resolvedSeatBids = new ArrayList<>(); final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); - for (final Map.Entry storedIdToImpId : idToStoredResponses.entrySet()) { + for (Map.Entry storedIdToImpId : idToStoredResponses.entrySet()) { final String id = storedIdToImpId.getKey(); final String rowSeatBid = storedIdToImpId.getValue(); if (rowSeatBid == null) { @@ -287,6 +220,25 @@ private List convertToSeatBid(StoredResponseDataResult storedResponseDa return mergeSameBidderSeatBid(resolvedSeatBids); } + private List resolveSeatBids(StoredResponse storedResponse, + Map idToStoredResponses, + String impId) { + + if (storedResponse instanceof StoredResponse.StoredResponseObject storedResponseObject) { + return Collections.singletonList(storedResponseObject.seatBid()); + } + + final String storedResponseId = ((StoredResponse.StoredResponseId) storedResponse).id(); + final String rowSeatBid = idToStoredResponses.get(storedResponseId); + if (rowSeatBid == null) { + throw new InvalidRequestException( + "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." + .formatted(impId, storedResponseId)); + } + + return parseSeatBid(storedResponseId, rowSeatBid); + } + private List parseSeatBid(String id, String rowSeatBid) { try { return mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPE); @@ -295,18 +247,6 @@ private List parseSeatBid(String id, String rowSeatBid) { } } - private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { - return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); - } - - private List updateBidsWithImpId(List bids, String impId) { - return bids.stream().map(bid -> updateBidWithImpId(bid, impId)).toList(); - } - - private static Bid updateBidWithImpId(Bid bid, String impId) { - return bid.toBuilder().impid(impId).build(); - } - private void validateStoredSeatBid(List seatBids) { for (final SeatBid seatBid : seatBids) { if (StringUtils.isEmpty(seatBid.getSeat())) { @@ -319,6 +259,18 @@ private void validateStoredSeatBid(List seatBids) { } } + private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { + return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); + } + + private List updateBidsWithImpId(List bids, String impId) { + return bids.stream().map(bid -> updateBidWithImpId(bid, impId)).toList(); + } + + private static Bid updateBidWithImpId(Bid bid, String impId) { + return bid.toBuilder().impid(impId).build(); + } + private List mergeSameBidderSeatBid(List seatBids) { return seatBids.stream().collect(Collectors.groupingBy(SeatBid::getSeat, Collectors.toList())) .entrySet().stream() @@ -336,23 +288,108 @@ private SeatBid makeMergedSeatBid(String seat, List storedSeatBids) { private Map> mapStoredBidResponseIdsToValues( Map idToStoredResponses, - Map> impToBidderToStoredBidResponseId) { + Map> impToBidderToStoredBidResponseId) { return impToBidderToStoredBidResponseId.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().entrySet().stream() - .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue())) + .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue().id())) .collect(Collectors.toMap( Map.Entry::getKey, - bidderToId -> idToStoredResponses.get(bidderToId.getValue()), + bidderToId -> idToStoredResponses.get(bidderToId.getValue().id()), (first, second) -> second, CaseInsensitiveMap::new)))); } + public List updateStoredBidResponse(List auctionParticipations) { + return auctionParticipations.stream() + .map(StoredResponseProcessor::updateStoredBidResponse) + .collect(Collectors.toList()); + } + + private static AuctionParticipation updateStoredBidResponse(AuctionParticipation auctionParticipation) { + final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + + final List imps = bidRequest.getImp(); + // ะor now, Stored Bid Response works only for bid requests with single imp + if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid initialSeatBid = bidderResponse.getSeatBid(); + final BidderSeatBid adjustedSeatBid = updateSeatBid(initialSeatBid, imps.getFirst().getId()); + + return auctionParticipation.with(bidderResponse.with(adjustedSeatBid)); + } + + private static BidderSeatBid updateSeatBid(BidderSeatBid bidderSeatBid, String impId) { + final List bids = bidderSeatBid.getBids().stream() + .map(bidderBid -> resolveBidImpId(bidderBid, impId)) + .collect(Collectors.toList()); + + return bidderSeatBid.with(bids); + } + + private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { + final Bid bid = bidderBid.getBid(); + final String bidImpId = bid.getImpid(); + if (!StringUtils.contains(bidImpId, PBS_IMPID_MACRO)) { + return bidderBid; + } + + return bidderBid.toBuilder() + .bid(bid.toBuilder().impid(bidImpId.replace(PBS_IMPID_MACRO, impId)).build()) + .build(); + } + + List mergeWithBidderResponses(List auctionParticipations, + List storedAuctionResponses, + List imps, + Map bidRejectionTrackers) { + + if (CollectionUtils.isEmpty(storedAuctionResponses)) { + return auctionParticipations; + } + + final Map bidderToAuctionParticipation = auctionParticipations.stream() + .collect(Collectors.toMap(AuctionParticipation::getBidder, Function.identity())); + final Map bidderToSeatBid = storedAuctionResponses.stream() + .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); + final Map impIdToBidType = imps.stream() + .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); + final Set responseBidders = new HashSet<>(bidderToAuctionParticipation.keySet()); + responseBidders.addAll(bidderToSeatBid.keySet()); + + return responseBidders.stream() + .map(bidder -> updateBidderResponse( + bidderToAuctionParticipation.get(bidder), + bidderToSeatBid.get(bidder), + impIdToBidType)) + .map(auctionParticipation -> restoreStoredBidsFromRejection(bidRejectionTrackers, auctionParticipation)) + .toList(); + } + + private BidType resolveBidType(Imp imp) { + BidType bidType = BidType.banner; + if (imp.getBanner() != null) { + return bidType; + } else if (imp.getVideo() != null) { + bidType = BidType.video; + } else if (imp.getXNative() != null) { + bidType = BidType.xNative; + } else if (imp.getAudio() != null) { + bidType = BidType.audio; + } + return bidType; + } + private AuctionParticipation updateBidderResponse(AuctionParticipation auctionParticipation, SeatBid storedSeatBid, Map impIdToBidType) { + if (auctionParticipation != null) { if (auctionParticipation.isRequestBlocked()) { return auctionParticipation; @@ -377,13 +414,17 @@ private AuctionParticipation updateBidderResponse(AuctionParticipation auctionPa } } - private BidderSeatBid makeBidderSeatBid(BidderSeatBid bidderSeatBid, SeatBid seatBid, + private BidderSeatBid makeBidderSeatBid(BidderSeatBid bidderSeatBid, + SeatBid seatBid, Map impIdToBidType) { + final boolean nonNullBidderSeatBid = bidderSeatBid != null; final String bidCurrency = nonNullBidderSeatBid ? bidderSeatBid.getBids().stream() - .map(BidderBid::getBidCurrency).filter(Objects::nonNull) - .findAny().orElse(DEFAULT_BID_CURRENCY) + .map(BidderBid::getBidCurrency) + .filter(Objects::nonNull) + .findAny() + .orElse(DEFAULT_BID_CURRENCY) : DEFAULT_BID_CURRENCY; final List bidderBids = seatBid != null ? seatBid.getBid().stream() @@ -416,17 +457,28 @@ private ExtBidPrebid parseExtBidPrebid(ObjectNode bidExtPrebid) { } } - private BidType resolveBidType(Imp imp) { - BidType bidType = BidType.banner; - if (imp.getBanner() != null) { - return bidType; - } else if (imp.getVideo() != null) { - bidType = BidType.video; - } else if (imp.getXNative() != null) { - bidType = BidType.xNative; - } else if (imp.getAudio() != null) { - bidType = BidType.audio; + private static AuctionParticipation restoreStoredBidsFromRejection( + Map bidRejectionTrackers, + AuctionParticipation auctionParticipation) { + + final BidRejectionTracker bidRejectionTracker = bidRejectionTrackers.get(auctionParticipation.getBidder()); + + if (bidRejectionTracker != null) { + Optional.ofNullable(auctionParticipation.getBidderResponse()) + .map(BidderResponse::getSeatBid) + .map(BidderSeatBid::getBids) + .ifPresent(bidRejectionTracker::restoreFromRejection); + } + + return auctionParticipation; + } + + private sealed interface StoredResponse { + + record StoredResponseId(String id) implements StoredResponse { + } + + record StoredResponseObject(SeatBid seatBid) implements StoredResponse { } - return bidType; } } diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 9472f734336..2896e153adf 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -32,10 +32,6 @@ public class TargetingKeywordsCreator { * It will exist only if the incoming bidRequest defiend request.app instead of request.site. */ private static final String ENV_KEY = "_env"; - /** - * Used as a value for ENV_KEY. - */ - private static final String ENV_APP_VALUE = "mobile-app"; /** * Name of the Bidder. For example, "appnexus" or "rubicon". */ @@ -87,7 +83,7 @@ public class TargetingKeywordsCreator { private final boolean includeBidderKeys; private final boolean alwaysIncludeDeals; private final boolean includeFormat; - private final boolean isApp; + private final String env; private final int truncateAttrChars; private final String cacheHost; private final String cachePath; @@ -99,7 +95,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -111,7 +107,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, this.includeBidderKeys = includeBidderKeys; this.alwaysIncludeDeals = alwaysIncludeDeals; this.includeFormat = includeFormat; - this.isApp = isApp; + this.env = env; this.truncateAttrChars = truncateAttrChars; this.cacheHost = cacheHost; this.cachePath = cachePath; @@ -127,7 +123,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -139,7 +135,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul includeBidderKeys, alwaysIncludeDeals, includeFormat, - isApp, + env, truncateAttrChars, cacheHost, cachePath, @@ -230,8 +226,8 @@ private Map makeFor(String bidder, if (StringUtils.isNotBlank(dealId)) { keywordMap.put(this.keyPrefix + DEAL_KEY, dealId); } - if (isApp) { - keywordMap.put(this.keyPrefix + ENV_KEY, ENV_APP_VALUE); + if (env != null) { + keywordMap.put(this.keyPrefix + ENV_KEY, env); } if (StringUtils.isNotBlank(categoryDuration)) { keywordMap.put(this.keyPrefix + CATEGORY_DURATION_KEY, categoryDuration); diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index 84ebacc7416..f6bcb5599af 100644 --- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java @@ -24,7 +24,7 @@ import org.prebid.server.auction.model.Tuple2; import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java index b13cf522b49..e9a7b7818a1 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -28,7 +28,7 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtDealTier; diff --git a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java index 088a9604b9d..2c3b3f369b0 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java @@ -4,7 +4,7 @@ import io.vertx.core.Future; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import java.util.List; diff --git a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java index f6161fa6f90..88ac988c521 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java @@ -4,7 +4,7 @@ import io.vertx.core.Future; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import java.util.List; diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java index f4e89ea2f76..af0a9e2428c 100644 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java +++ b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.model.BidderError; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -78,10 +80,11 @@ private MediaType preferredMediaType(BidRequest bidRequest, Account account, String originalBidderName, String resolvedBidderName) { + return Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) .map(ExtRequestPrebid::getBiddercontrols) - .map(bidders -> bidders.get(originalBidderName)) + .map(bidders -> getBidder(originalBidderName, bidders)) .map(bidder -> bidder.get(PREF_MTYPE_FIELD)) .filter(JsonNode::isTextual) .map(JsonNode::textValue) @@ -92,6 +95,17 @@ private MediaType preferredMediaType(BidRequest bidRequest, .orElse(null); } + private static JsonNode getBidder(String bidderName, JsonNode biddersNode) { + final Iterator fieldNames = biddersNode.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (StringUtils.equalsIgnoreCase(bidderName, fieldName)) { + return biddersNode.get(fieldName); + } + } + return null; + } + private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { if (!isMultiFormat(imp)) { return imp; diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 3ee60aab4fa..5dbe83c3ff2 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -8,6 +8,7 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.geolocation.model.GeoInfo; @@ -17,6 +18,7 @@ import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -71,6 +73,10 @@ public class AuctionContext { CachedDebugLog cachedDebugLog; + @JsonIgnore + @Builder.Default + BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap()); + public AuctionContext with(Account account) { return this.toBuilder().account(account).build(); } @@ -124,6 +130,12 @@ public AuctionContext with(GeoInfo geoInfo) { .build(); } + public AuctionContext with(BidAdjustments bidAdjustments) { + return this.toBuilder() + .bidAdjustments(bidAdjustments) + .build(); + } + public AuctionContext withRequestRejected() { return this.toBuilder() .requestRejected(true) diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java index 1cb95bcf681..aa3be49fd48 100644 --- a/src/main/java/org/prebid/server/auction/model/BidInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -33,7 +33,7 @@ public class BidInfo { Integer ttl; - Integer videoTtl; + Integer vastTtl; public String getBidId() { final ObjectNode extNode = bid != null ? bid.getExt() : null; diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index de2e89efc31..fc3ee36bd2a 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -1,37 +1,120 @@ package org.prebid.server.auction.model; import com.fasterxml.jackson.annotation.JsonValue; -import org.prebid.server.bidder.model.BidderError; +/** + * The list of the Seat Non Bid codes: + * 0 - the bidder is called but declines to bid and doesn't provide a code (for the impression) + * 100-199 - the bidder is called but returned with an unspecified error (for the impression) + * 200-299 - the bidder is not called at all + * 300-399 - the bidder is called, but its response is rejected + */ public enum BidRejectionReason { + /** + * If the bidder returns in time but declines to bid and doesnโ€™t provide an โ€œNBRโ€ code. + */ NO_BID(0), - TIMED_OUT(101), - REJECTED_BY_HOOK(200), - REJECTED_BY_PRIVACY(202), - REJECTED_BY_MEDIA_TYPE(204), - GENERAL(300), - REJECTED_DUE_TO_PRICE_FLOOR(301), - REJECTED_BY_DSA_PRIVACY(305), - FAILED_TO_REQUEST_BIDS(100), - OTHER_ERROR(100); - - public final int code; + + /** + * The bidder returned with an unspecified error for this impression. + * Applied if any other ERROR is not recognized. + */ + ERROR_GENERAL(100), + + /** + * The bidder failed because of timeout + */ + ERROR_TIMED_OUT(101), + + /** + * The bidder returned status code less than 200 OR greater than or equal to 400 + */ + ERROR_INVALID_BID_RESPONSE(102), + + /** + * The bidder returned HTTP 503 + */ + ERROR_BIDDER_UNREACHABLE(103), + + /** + * The bidder is not called at all. + * Applied if any other REQUEST_BLOCKED reason is not recognized. + */ + REQUEST_BLOCKED_GENERAL(200), + + /** + * If the request was not sent to the bidder because they donโ€™t support dooh or app + */ + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + + /** + * This impression not sent to the bid adapter because it doesnโ€™t support the requested mediatype. + */ + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + + /** + * If the bidder was not called due to GDPR purpose 2 + */ + REQUEST_BLOCKED_PRIVACY(204), + + /** + * If the bidder was not called due to a mismatch between the bidderโ€™s currency and the requestโ€™s currency. + */ + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), + + /** + * The bidder is called, but its response is rejected. + * Applied if any other RESPONSE_REJECTED reason is not recognized. + */ + RESPONSE_REJECTED_GENERAL(300), + + /** + * The bidder returns a bid that doesn't meet the price floor. + */ + RESPONSE_REJECTED_BELOW_FLOOR(301), + + /** + * The bidder returns a bid that doesn't meet the price deal floor. + */ + RESPONSE_REJECTED_BELOW_DEAL_FLOOR(304), + + /** + * Rejected by the DSA validations + */ + RESPONSE_REJECTED_DSA_PRIVACY(305), + + /** + * If the ortbblocking module enforced a bid response for battr, bcat, bapp, btype. + * If the richmedia module filtered out a bid response. + */ + RESPONSE_REJECTED_INVALID_CREATIVE(350), + + /** + * If a bid response was rejected due to size. + * When the auction.bid-validations.banner-creative-max-size is in enforce mode and rejects a bid. + */ + RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED(351), + + /** + * If a bid response was rejected due to auction.validations.secure-markup + */ + RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE(352), + + /** + * If the ortbblocking module enforced a bid response due to badv + */ + RESPONSE_REJECTED_ADVERTISER_BLOCKED(356); + + private final int code; BidRejectionReason(int code) { this.code = code; } @JsonValue - private int getValue() { + public int getValue() { return code; } - public static BidRejectionReason fromBidderError(BidderError error) { - return switch (error.getType()) { - case timeout -> BidRejectionReason.TIMED_OUT; - case rejected_ipf -> BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR; - default -> BidRejectionReason.OTHER_ERROR; - }; - } } diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java index b0504ec13a3..9810606ada3 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java @@ -1,90 +1,162 @@ package org.prebid.server.auction.model; import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.MapUtil; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class BidRejectionTracker { private static final Logger logger = LoggerFactory.getLogger(BidRejectionTracker.class); - private static final ConditionalLogger MULTIPLE_BID_REJECTIONS_LOGGER = + private static final ConditionalLogger BID_REJECTIONS_LOGGER = new ConditionalLogger("multiple-bid-rejections", logger); - private static final String WARNING_TEMPLATE = - "Bid with imp id: %s for bidder: %s rejected due to: %s, but has been already rejected"; + private static final String MULTIPLE_REJECTIONS_WARNING_TEMPLATE = + "Warning: bidder %s on imp %s responded with multiple nonbid reasons."; + + private static final String INCONSISTENT_RESPONSES_WARNING_TEMPLATE = + "Warning: Inconsistent responses from bidder %s on imp %s: both bids and nonbids."; private final double logSamplingRate; private final String bidder; private final Set involvedImpIds; - private final Set succeededImpIds; - private final Map rejectedImpIds; + private final Map> succeededBidsIds; + private final Map>> rejectedBids; public BidRejectionTracker(String bidder, Set involvedImpIds, double logSamplingRate) { this.bidder = bidder; this.involvedImpIds = new HashSet<>(involvedImpIds); this.logSamplingRate = logSamplingRate; - succeededImpIds = new HashSet<>(); - rejectedImpIds = new HashMap<>(); - } - - public void succeed(String impId) { - if (involvedImpIds.contains(impId)) { - succeededImpIds.add(impId); - rejectedImpIds.remove(impId); - } + succeededBidsIds = new HashMap<>(); + rejectedBids = new HashMap<>(); } + /** + * Restores ONLY imps from rejection, rejected bids are preserved for analytics. + * A bid can be rejected only once. + */ public void succeed(Collection bids) { bids.stream() .map(BidderBid::getBid) .filter(Objects::nonNull) - .map(Bid::getImpid) - .filter(Objects::nonNull) .forEach(this::succeed); } + private void succeed(Bid bid) { + final String bidId = bid.getId(); + final String impId = bid.getImpid(); + if (involvedImpIds.contains(impId)) { + succeededBidsIds.computeIfAbsent(impId, key -> new HashSet<>()).add(bidId); + if (rejectedBids.containsKey(impId)) { + BID_REJECTIONS_LOGGER.warn( + INCONSISTENT_RESPONSES_WARNING_TEMPLATE.formatted(bidder, impId), + logSamplingRate); + } + } + } + public void restoreFromRejection(Collection bids) { succeed(bids); } - public void reject(String impId, BidRejectionReason reason) { - if (involvedImpIds.contains(impId) && !rejectedImpIds.containsKey(impId)) { - rejectedImpIds.put(impId, reason); - succeededImpIds.remove(impId); - } else if (rejectedImpIds.containsKey(impId)) { - MULTIPLE_BID_REJECTIONS_LOGGER.warn( - WARNING_TEMPLATE.formatted(impId, bidder, reason), logSamplingRate); + public void rejectBids(Collection bidderBids, BidRejectionReason reason) { + bidderBids.forEach(bidderBid -> rejectBid(bidderBid, reason)); + } + + public void rejectBid(BidderBid bidderBid, BidRejectionReason reason) { + final Bid bid = bidderBid.getBid(); + final String impId = bid.getImpid(); + + reject(impId, bidderBid, reason); + } + + private void reject(String impId, BidderBid bid, BidRejectionReason reason) { + if (involvedImpIds.contains(impId)) { + if (rejectedBids.containsKey(impId)) { + BID_REJECTIONS_LOGGER.warn( + MULTIPLE_REJECTIONS_WARNING_TEMPLATE.formatted(bidder, impId), logSamplingRate); + } + + rejectedBids.computeIfAbsent(impId, key -> new ArrayList<>()).add(Pair.of(bid, reason)); + + if (succeededBidsIds.containsKey(impId)) { + final String bidId = Optional.ofNullable(bid).map(BidderBid::getBid).map(Bid::getId).orElse(null); + final Set succeededBids = succeededBidsIds.get(impId); + final boolean removed = bidId == null || succeededBids.remove(bidId); + if (removed && !succeededBids.isEmpty()) { + BID_REJECTIONS_LOGGER.warn( + INCONSISTENT_RESPONSES_WARNING_TEMPLATE.formatted(bidder, impId), + logSamplingRate); + } + } + } + } + + public void rejectImps(Collection impIds, BidRejectionReason reason) { + impIds.forEach(impId -> rejectImp(impId, reason)); + } + + public void rejectImp(String impId, BidRejectionReason reason) { + if (reason.getValue() >= 300) { + throw new IllegalArgumentException("The non-bid code 300 and higher assumes " + + "that there is a rejected bid that shouldn't be lost"); } + reject(impId, null, reason); } - public void reject(Collection impIds, BidRejectionReason reason) { - impIds.forEach(impId -> reject(impId, reason)); + public void rejectAllImps(BidRejectionReason reason) { + involvedImpIds.forEach(impId -> rejectImp(impId, reason)); } - public void rejectAll(BidRejectionReason reason) { - involvedImpIds.forEach(impId -> reject(impId, reason)); + /** + * If an impression has at least one valid bid, it's not considered rejected. + * If no valid bids are returned for the impression, only the first one rejected reason will be returned + */ + public Map getRejectedImps() { + final Map rejectedImpIds = new HashMap<>(); + for (String impId : involvedImpIds) { + final Set succeededBids = succeededBidsIds.getOrDefault(impId, Collections.emptySet()); + if (succeededBids.isEmpty()) { + if (rejectedBids.containsKey(impId)) { + rejectedImpIds.put(impId, rejectedBids.get(impId).getFirst().getRight()); + } else { + rejectedImpIds.put(impId, BidRejectionReason.NO_BID); + } + } + } + + return rejectedImpIds; } - public Map getRejectionReasons() { - final Map missingImpIds = new HashMap<>(); + /** + * Bid is absent for the non-bid code from 0 to 299 + */ + public Map>> getRejectedBids() { + final Map>> missingImpIds = new HashMap<>(); for (String impId : involvedImpIds) { - if (!succeededImpIds.contains(impId) && !rejectedImpIds.containsKey(impId)) { - missingImpIds.put(impId, BidRejectionReason.NO_BID); + final Set succeededBids = succeededBidsIds.getOrDefault(impId, Collections.emptySet()); + if (succeededBids.isEmpty() && !rejectedBids.containsKey(impId)) { + missingImpIds.computeIfAbsent(impId, key -> new ArrayList<>()) + .add(Pair.of(null, BidRejectionReason.NO_BID)); } } - return MapUtil.merge(missingImpIds, rejectedImpIds); + return MapUtil.merge(missingImpIds, rejectedBids); } } diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java index d045b0ceadc..05c451bf863 100644 --- a/src/main/java/org/prebid/server/auction/model/SetuidContext.java +++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.bidder.UsersyncMethodType; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java index b8379056afa..87c390e6b2e 100644 --- a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java +++ b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java @@ -1,7 +1,7 @@ package org.prebid.server.auction.model; import lombok.Value; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; @Value(staticConstructor = "of") public class TimeoutContext { diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java index 55e32fd1372..0e1e6cbab13 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java @@ -6,7 +6,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.metric.MetricName; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java index 3e81b3fcf4f..b637e656290 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java @@ -6,7 +6,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.metric.MetricName; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java index df7603048ee..395dff73802 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java @@ -12,6 +12,7 @@ import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; import org.prebid.server.activity.infrastructure.payload.impl.PrivacyEnforcementServiceActivityInvocationPayload; +import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; @@ -21,7 +22,7 @@ import java.util.Objects; import java.util.Optional; -public class ActivityEnforcement { +public class ActivityEnforcement implements PrivacyEnforcement { private final UserFpdActivityMask userFpdActivityMask; @@ -29,17 +30,19 @@ public ActivityEnforcement(UserFpdActivityMask userFpdActivityMask) { this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } - public Future> enforce(List bidderPrivacyResults, - AuctionContext auctionContext) { + @Override + public Future> enforce(AuctionContext auctionContext, + BidderAliases aliases, + List results) { - final List results = bidderPrivacyResults.stream() + final List enforcedResults = results.stream() .map(bidderPrivacyResult -> applyActivityRestrictions( bidderPrivacyResult, auctionContext.getActivityInfrastructure(), auctionContext.getBidRequest())) .toList(); - return Future.succeededFuture(results); + return Future.succeededFuture(enforcedResults); } private BidderPrivacyResult applyActivityRestrictions(BidderPrivacyResult bidderPrivacyResult, diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java index 10fe183801d..51a8d8f4622 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java @@ -1,10 +1,7 @@ package org.prebid.server.auction.privacy.enforcement; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.User; import io.vertx.core.Future; -import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; @@ -22,12 +19,12 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; -public class CcpaEnforcement { +public class CcpaEnforcement implements PrivacyEnforcement { private static final String CATCH_ALL_BIDDERS = "*"; @@ -47,9 +44,10 @@ public CcpaEnforcement(UserFpdCcpaMask userFpdCcpaMask, this.ccpaEnforce = ccpaEnforce; } + @Override public Future> enforce(AuctionContext auctionContext, - Map bidderToUser, - BidderAliases aliases) { + BidderAliases aliases, + List results) { final Ccpa ccpa = auctionContext.getPrivacyContext().getPrivacy().getCcpa(); final BidRequest bidRequest = auctionContext.getBidRequest(); @@ -58,7 +56,7 @@ public Future> enforce(AuctionContext auctionContext, final boolean isCcpaEnabled = isCcpaEnabled(auctionContext.getAccount(), auctionContext.getRequestTypeMetric()); final Set enforcedBidders = isCcpaEnabled && isCcpaEnforced - ? extractCcpaEnforcedBidders(bidderToUser.keySet(), bidRequest, aliases) + ? extractCcpaEnforcedBidders(results, bidRequest, aliases) : Collections.emptySet(); metrics.updatePrivacyCcpaMetrics( @@ -68,7 +66,11 @@ public Future> enforce(AuctionContext auctionContext, isCcpaEnabled, enforcedBidders); - return Future.succeededFuture(maskCcpa(bidderToUser, enforcedBidders, bidRequest.getDevice())); + final List enforcedResults = results.stream() + .map(result -> enforcedBidders.contains(result.getRequestBidder()) ? maskCcpa(result) : result) + .toList(); + + return Future.succeededFuture(enforcedResults); } public boolean isCcpaEnforced(Ccpa ccpa, Account account) { @@ -79,19 +81,21 @@ private boolean isCcpaEnabled(Account account, MetricName requestType) { final Optional accountCcpaConfig = Optional.ofNullable(account.getPrivacy()) .map(AccountPrivacyConfig::getCcpa); - return ObjectUtils.firstNonNull( - accountCcpaConfig - .map(AccountCcpaConfig::getEnabledForRequestType) - .map(enabledForRequestType -> enabledForRequestType.isEnabledFor(requestType)) - .orElse(null), - accountCcpaConfig - .map(AccountCcpaConfig::getEnabled) - .orElse(null), - ccpaEnforce); + return accountCcpaConfig + .map(AccountCcpaConfig::getEnabledForRequestType) + .map(enabledForRequestType -> enabledForRequestType.isEnabledFor(requestType)) + .or(() -> accountCcpaConfig.map(AccountCcpaConfig::getEnabled)) + .orElse(ccpaEnforce); } - private Set extractCcpaEnforcedBidders(Set bidders, BidRequest bidRequest, BidderAliases aliases) { - final Set ccpaEnforcedBidders = new HashSet<>(bidders); + private Set extractCcpaEnforcedBidders(List results, + BidRequest bidRequest, + BidderAliases aliases) { + + final Set ccpaEnforcedBidders = results.stream() + .map(BidderPrivacyResult::getRequestBidder) + .collect(Collectors.toCollection(HashSet::new)); + final List nosaleBidders = Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) .map(ExtRequestPrebid::getNosale) @@ -109,14 +113,11 @@ private Set extractCcpaEnforcedBidders(Set bidders, BidRequest b return ccpaEnforcedBidders; } - private List maskCcpa(Map bidderToUser, Set bidders, Device device) { - final Device maskedDevice = userFpdCcpaMask.maskDevice(device); - return bidders.stream() - .map(bidder -> BidderPrivacyResult.builder() - .requestBidder(bidder) - .user(userFpdCcpaMask.maskUser(bidderToUser.get(bidder))) - .device(maskedDevice) - .build()) - .toList(); + private BidderPrivacyResult maskCcpa(BidderPrivacyResult result) { + return BidderPrivacyResult.builder() + .requestBidder(result.getRequestBidder()) + .user(userFpdCcpaMask.maskUser(result.getUser())) + .device(userFpdCcpaMask.maskDevice(result.getDevice())) + .build(); } } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java index 92471e85ec2..9ebe0c8d044 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java @@ -1,18 +1,18 @@ package org.prebid.server.auction.privacy.enforcement; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.User; import io.vertx.core.Future; +import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask; import org.prebid.server.metric.Metrics; import java.util.List; -import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; -public class CoppaEnforcement { +public class CoppaEnforcement implements PrivacyEnforcement { private final UserFpdCoppaMask userFpdCoppaMask; private final Metrics metrics; @@ -22,23 +22,34 @@ public CoppaEnforcement(UserFpdCoppaMask userFpdCoppaMask, Metrics metrics) { this.metrics = Objects.requireNonNull(metrics); } - public boolean isApplicable(AuctionContext auctionContext) { - return auctionContext.getPrivacyContext().getPrivacy().getCoppa() == 1; - } + @Override + public Future> enforce(AuctionContext auctionContext, + BidderAliases aliases, + List results) { + + if (!isApplicable(auctionContext)) { + return Future.succeededFuture(results); + } + + final Set bidders = results.stream() + .map(BidderPrivacyResult::getRequestBidder) + .collect(Collectors.toSet()); - public Future> enforce(AuctionContext auctionContext, Map bidderToUser) { - metrics.updatePrivacyCoppaMetric(auctionContext.getActivityInfrastructure(), bidderToUser.keySet()); - return Future.succeededFuture(results(bidderToUser, auctionContext.getBidRequest().getDevice())); + metrics.updatePrivacyCoppaMetric(auctionContext.getActivityInfrastructure(), bidders); + return Future.succeededFuture(enforce(results)); } - private List results(Map bidderToUser, Device device) { - final Device maskedDevice = userFpdCoppaMask.maskDevice(device); - return bidderToUser.entrySet().stream() - .map(bidderAndUser -> BidderPrivacyResult.builder() - .requestBidder(bidderAndUser.getKey()) - .user(userFpdCoppaMask.maskUser(bidderAndUser.getValue())) - .device(maskedDevice) + private List enforce(List results) { + return results.stream() + .map(result -> BidderPrivacyResult.builder() + .requestBidder(result.getRequestBidder()) + .user(userFpdCoppaMask.maskUser(result.getUser())) + .device(userFpdCoppaMask.maskDevice(result.getDevice())) .build()) .toList(); } + + private static boolean isApplicable(AuctionContext auctionContext) { + return auctionContext.getPrivacyContext().getPrivacy().getCoppa() == 1; + } } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java new file mode 100644 index 00000000000..d12e290fb2e --- /dev/null +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java @@ -0,0 +1,15 @@ +package org.prebid.server.auction.privacy.enforcement; + +import io.vertx.core.Future; +import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderPrivacyResult; + +import java.util.List; + +public interface PrivacyEnforcement { + + Future> enforce(AuctionContext auctionContext, + BidderAliases aliases, + List results); +} diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java index 3f4e4055dca..f50a7c637f6 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java @@ -5,58 +5,41 @@ import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; -import org.prebid.server.util.ListUtil; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Service provides masking for OpenRTB client sensitive information. */ public class PrivacyEnforcementService { - private final CoppaEnforcement coppaEnforcement; - private final CcpaEnforcement ccpaEnforcement; - private final TcfEnforcement tcfEnforcement; - private final ActivityEnforcement activityEnforcement; + private final List enforcements; - public PrivacyEnforcementService(CoppaEnforcement coppaEnforcement, - CcpaEnforcement ccpaEnforcement, - TcfEnforcement tcfEnforcement, - ActivityEnforcement activityEnforcement) { - - this.coppaEnforcement = Objects.requireNonNull(coppaEnforcement); - this.ccpaEnforcement = Objects.requireNonNull(ccpaEnforcement); - this.tcfEnforcement = Objects.requireNonNull(tcfEnforcement); - this.activityEnforcement = Objects.requireNonNull(activityEnforcement); + public PrivacyEnforcementService(final List enforcements) { + this.enforcements = Objects.requireNonNull(enforcements); } public Future> mask(AuctionContext auctionContext, Map bidderToUser, BidderAliases aliases) { - // For now, COPPA masking all values, so we can omit TCF masking. - return coppaEnforcement.isApplicable(auctionContext) - ? coppaEnforcement.enforce(auctionContext, bidderToUser) - : ccpaEnforcement.enforce(auctionContext, bidderToUser, aliases) - .compose(ccpaResult -> tcfEnforcement.enforce( - auctionContext, - bidderToUser, - biddersToApplyTcf(bidderToUser.keySet(), ccpaResult), - aliases) - .map(tcfResult -> ListUtil.union(ccpaResult, tcfResult))) - .compose(bidderPrivacyResults -> activityEnforcement.enforce(bidderPrivacyResults, auctionContext)); - } + final List initialResults = bidderToUser.entrySet().stream() + .map(entry -> BidderPrivacyResult.builder() + .requestBidder(entry.getKey()) + .user(entry.getValue()) + .device(auctionContext.getBidRequest().getDevice()) + .build()) + .toList(); + + Future> composedResult = Future.succeededFuture(initialResults); - private static Set biddersToApplyTcf(Set bidders, List ccpaResult) { - final Set biddersToApplyTcf = new HashSet<>(bidders); - ccpaResult.stream() - .map(BidderPrivacyResult::getRequestBidder) - .forEach(biddersToApplyTcf::remove); + for (PrivacyEnforcement enforcement : enforcements) { + composedResult = composedResult.compose( + results -> enforcement.enforce(auctionContext, aliases, results)); + } - return biddersToApplyTcf; + return composedResult; } } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java index 48e098f63a6..86602099401 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java @@ -30,8 +30,9 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; -public class TcfEnforcement { +public class TcfEnforcement implements PrivacyEnforcement { private static final Logger logger = LoggerFactory.getLogger(TcfEnforcement.class); @@ -59,30 +60,25 @@ public Future> enforce(Set vendo .map(TcfResponse::getActions); } + @Override public Future> enforce(AuctionContext auctionContext, - Map bidderToUser, - Set bidders, - BidderAliases aliases) { + BidderAliases aliases, + List results) { - final Device device = auctionContext.getBidRequest().getDevice(); - final AccountGdprConfig accountGdprConfig = accountGdprConfig(auctionContext.getAccount()); final MetricName requestType = auctionContext.getRequestTypeMetric(); final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + final Set bidders = results.stream() + .map(BidderPrivacyResult::getRequestBidder) + .collect(Collectors.toSet()); return tcfDefinerService.resultForBidderNames( bidders, VendorIdResolver.of(aliases, bidderCatalog), auctionContext.getPrivacyContext().getTcfContext(), - accountGdprConfig) + accountGdprConfig(auctionContext.getAccount())) .map(TcfResponse::getActions) - .map(enforcements -> updateMetrics( - activityInfrastructure, - enforcements, - aliases, - requestType, - bidderToUser, - device)) - .map(enforcements -> bidderToPrivacyResult(enforcements, bidders, bidderToUser, device)); + .map(enforcements -> updateMetrics(activityInfrastructure, enforcements, aliases, requestType, results)) + .map(enforcements -> applyEnforcements(enforcements, results)); } private static AccountGdprConfig accountGdprConfig(Account account) { @@ -94,22 +90,21 @@ private Map updateMetrics(ActivityInfrastructu Map enforcements, BidderAliases aliases, MetricName requestType, - Map bidderToUser, - Device device) { - - final boolean isLmtEnforcedAndEnabled = isLmtEnforcedAndEnabled(device); + List results) { // Metrics should represent real picture of the bidding process, so if bidder request is blocked // by privacy then no reason to increment another metrics, like geo masked, etc. - for (final Map.Entry bidderEnforcement : enforcements.entrySet()) { - final String bidder = bidderEnforcement.getKey(); - final PrivacyEnforcementAction enforcement = bidderEnforcement.getValue(); - final User user = bidderToUser.get(bidder); + for (BidderPrivacyResult result : results) { + final String bidder = result.getRequestBidder(); + final User user = result.getUser(); + final Device device = result.getDevice(); + final PrivacyEnforcementAction enforcement = enforcements.get(bidder); final boolean requestBlocked = enforcement.isBlockBidderRequest(); final boolean ufpdRemoved = !requestBlocked && ((enforcement.isRemoveUserFpd() && shouldRemoveUserData(user)) || (enforcement.isMaskDeviceInfo() && shouldRemoveDeviceData(device))); + final boolean isLmtEnforcedAndEnabled = isLmtEnforcedAndEnabled(device); final boolean uidsRemoved = !requestBlocked && enforcement.isRemoveUserIds() && shouldRemoveUids(user); final boolean geoMasked = !requestBlocked && enforcement.isMaskGeo() && shouldMaskGeo(user, device); final boolean analyticsBlocked = !requestBlocked && enforcement.isBlockAnalyticsReport(); @@ -165,32 +160,19 @@ private boolean isLmtEnforcedAndEnabled(Device device) { return lmtEnforce && device != null && Objects.equals(device.getLmt(), 1); } - private List bidderToPrivacyResult(Map bidderToEnforcement, - Set bidders, - Map bidderToUser, - Device device) { - - final boolean isLmtEnabled = isLmtEnforcedAndEnabled(device); + private List applyEnforcements(Map enforcements, + List results) { - return bidders.stream() - .map(bidder -> createBidderPrivacyResult( - bidder, - bidderToUser.get(bidder), - device, - bidderToEnforcement, - isLmtEnabled)) + return results.stream() + .map(result -> applyEnforcement(enforcements.get(result.getRequestBidder()), result)) .toList(); } - private BidderPrivacyResult createBidderPrivacyResult(String bidder, - User user, - Device device, - Map bidderToEnforcement, - boolean isLmtEnabled) { + private BidderPrivacyResult applyEnforcement(PrivacyEnforcementAction enforcement, BidderPrivacyResult result) { + final String bidder = result.getRequestBidder(); - final PrivacyEnforcementAction privacyEnforcementAction = bidderToEnforcement.get(bidder); - final boolean blockBidderRequest = privacyEnforcementAction.isBlockBidderRequest(); - final boolean blockAnalyticsReport = privacyEnforcementAction.isBlockAnalyticsReport(); + final boolean blockBidderRequest = enforcement.isBlockBidderRequest(); + final boolean blockAnalyticsReport = enforcement.isBlockAnalyticsReport(); if (blockBidderRequest) { return BidderPrivacyResult.builder() @@ -200,14 +182,18 @@ private BidderPrivacyResult createBidderPrivacyResult(String bidder, .build(); } - final boolean maskUserFpd = privacyEnforcementAction.isRemoveUserFpd() || isLmtEnabled; - final boolean maskUserIds = privacyEnforcementAction.isRemoveUserIds() || isLmtEnabled; - final boolean maskGeo = privacyEnforcementAction.isMaskGeo() || isLmtEnabled; - final Set eidExceptions = privacyEnforcementAction.getEidExceptions(); + final User user = result.getUser(); + final Device device = result.getDevice(); + + final boolean isLmtEnabled = isLmtEnforcedAndEnabled(device); + final boolean maskUserFpd = enforcement.isRemoveUserFpd() || isLmtEnabled; + final boolean maskUserIds = enforcement.isRemoveUserIds() || isLmtEnabled; + final boolean maskGeo = enforcement.isMaskGeo() || isLmtEnabled; + final Set eidExceptions = enforcement.getEidExceptions(); final User maskedUser = userFpdTcfMask.maskUser(user, maskUserFpd, maskUserIds, eidExceptions); - final boolean maskIp = privacyEnforcementAction.isMaskDeviceIp() || isLmtEnabled; - final boolean maskDeviceInfo = privacyEnforcementAction.isMaskDeviceInfo() || isLmtEnabled; + final boolean maskIp = enforcement.isMaskDeviceIp() || isLmtEnabled; + final boolean maskDeviceInfo = enforcement.isMaskDeviceInfo() || isLmtEnabled; final Device maskedDevice = userFpdTcfMask.maskDevice(device, maskIp, maskGeo, maskDeviceInfo); return BidderPrivacyResult.builder() diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index b5ac7cb4705..fb8187ed231 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -52,6 +52,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -60,6 +61,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; public class AmpRequestFactory { @@ -272,6 +274,7 @@ private static User createUser(ConsentParam consentParam, String addtlConsent) { final ExtUser extUser = consentedProvidersSettings != null ? ExtUser.builder() + .deprecatedConsentedProvidersSettings(consentedProvidersSettings) .consentedProvidersSettings(consentedProvidersSettings) .build() : null; @@ -407,9 +410,10 @@ private Future updateBidRequest(AuctionContext auctionContext) { .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) - .map(this::fillExplicitParameters) + .map(bidRequest -> fillExplicitParameters(bidRequest, account)) .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())) .compose(resolvedBidRequest -> ortb2RequestFactory.validateRequest( resolvedBidRequest, auctionContext.getHttpRequest(), @@ -458,7 +462,7 @@ private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidR * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext} * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters */ - private BidRequest fillExplicitParameters(BidRequest bidRequest) { + private BidRequest fillExplicitParameters(BidRequest bidRequest, Account account) { final List imps = bidRequest.getImp(); // Force HTTPS as AMP requires it, but pubs can forget to set it. final Imp imp = imps.getFirst(); @@ -495,6 +499,7 @@ private BidRequest fillExplicitParameters(BidRequest bidRequest) { .imp(setSecure ? Collections.singletonList(imps.getFirst().toBuilder().secure(1).build()) : imps) .ext(extRequest( bidRequest, + account, setDefaultTargeting, setDefaultCache)) .build(); @@ -691,6 +696,7 @@ private static List parseMultiSizeParam(String ms) { * Creates updated bidrequest.ext {@link ObjectNode}. */ private ExtRequest extRequest(BidRequest bidRequest, + Account account, boolean setDefaultTargeting, boolean setDefaultCache) { @@ -703,7 +709,7 @@ private ExtRequest extRequest(BidRequest bidRequest, : ExtRequestPrebid.builder(); if (setDefaultTargeting) { - prebidBuilder.targeting(createTargetingWithDefaults(prebid)); + prebidBuilder.targeting(createTargetingWithDefaults(prebid, account)); } if (setDefaultCache) { prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null), @@ -726,15 +732,14 @@ private ExtRequest extRequest(BidRequest bidRequest, * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's * child properties is missed or entire targeting does not exist. */ - private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) { + private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNull = targeting == null; final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity(); final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull(); - final JsonNode outgoingPriceGranularityNode - = isPriceGranularityNull - ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)) + final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull + ? mapper.mapper().valueToTree(ExtPriceGranularity.from(getDefaultPriceGranularity(account))) : priceGranularityNode; final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull @@ -758,6 +763,14 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) .build(); } + private static PriceGranularity getDefaultPriceGranularity(Account account) { + return Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + } + @Value(staticConstructor = "of") private static class GppSidExtraction { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 516205a2af0..34140a26228 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -17,6 +17,7 @@ import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; @@ -50,6 +51,7 @@ public class AuctionRequestFactory { private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; private final GeoLocationServiceWrapper geoLocationServiceWrapper; + private final BidAdjustmentsRetriever bidAdjustmentsRetriever; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); @@ -66,7 +68,8 @@ public AuctionRequestFactory(long maxRequestSize, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, JacksonMapper mapper, - GeoLocationServiceWrapper geoLocationServiceWrapper) { + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsRetriever bidAdjustmentsRetriever) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); @@ -82,6 +85,7 @@ public AuctionRequestFactory(long maxRequestSize, this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); + this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever); } /** @@ -142,6 +146,8 @@ public Future enrichAuctionContext(AuctionContext initialContext .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) .map(auctionContext::with)) + .map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext))) + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) @@ -248,7 +254,8 @@ private Future updateBidRequest(AuctionStoredResult auctionStoredRes .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, hasStoredBidRequest)) - .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)); + .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())); } private static MetricName requestTypeMetric(BidRequest bidRequest) { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java index 8b377c08bf9..5bcabe413db 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -56,6 +56,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -187,7 +189,11 @@ public BidRequest resolve(BidRequest bidRequest, final ExtRequest ext = bidRequest.getExt(); final List imps = bidRequest.getImp(); final ExtRequest populatedExt = populateRequestExt( - ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps), endpoint); + ext, + bidRequest, + ObjectUtils.defaultIfNull(populatedImps, imps), + endpoint, + auctionContext.getAccount()); final Source source = bidRequest.getSource(); final Source populatedSource = populateSource(source, populatedExt, hasStoredBidRequest); @@ -713,10 +719,15 @@ private static boolean isUniqueIds(List imps) { return impIdsSet.size() == impIdsList.size(); } - private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List imps, String endpoint) { + private ExtRequest populateRequestExt(ExtRequest ext, + BidRequest bidRequest, + List imps, + String endpoint, + Account account) { + final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(ext, ExtRequest::getPrebid); - final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps); + final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps, account); final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest, endpoint); @@ -783,7 +794,7 @@ private static void resolveImpMediaTypes(Imp imp, Set impsMediaTypes) { /** * Returns populated {@link ExtRequestTargeting} or null if no changes were applied. */ - private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps) { + private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNotNull = targeting != null; @@ -796,8 +807,12 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List i if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) { return targeting.toBuilder() - .pricegranularity(resolvePriceGranularity(targeting, isPriceGranularityNull, - isPriceGranularityTextual, imps)) + .pricegranularity(resolvePriceGranularity( + targeting, + isPriceGranularityNull, + isPriceGranularityTextual, + imps, + account)) .includewinners(isIncludeWinnersNull || targeting.getIncludewinners()) .includebidderkeys(isIncludeBidderKeysNull ? !isWinningOnly(prebid.getCache()) @@ -822,14 +837,22 @@ private boolean isWinningOnly(ExtRequestPrebidCache cache) { * In case of valid string price granularity replaced it with appropriate custom view. * In case of invalid string value throws {@link InvalidRequestException}. */ - private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, - boolean isPriceGranularityTextual, List imps) { + private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, + boolean isPriceGranularityNull, + boolean isPriceGranularityTextual, + List imps, + Account account) { final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) .containsAll(getImpMediaTypes(imps)); if (isPriceGranularityNull && !hasAllMediaTypes) { - return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); + final PriceGranularity defaultPriceGranularity = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + return mapper.mapper().valueToTree(ExtPriceGranularity.from(defaultPriceGranularity)); } final JsonNode priceGranularityNode = targeting.getPricegranularity(); diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index bd7411f24d7..01c4c8a43dc 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -4,10 +4,13 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.ext.web.RoutingContext; @@ -30,9 +33,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.floors.PriceFloorProcessor; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; @@ -51,11 +53,11 @@ import org.prebid.server.model.UpdateResult; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; -import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; @@ -73,12 +75,14 @@ import org.prebid.server.validation.model.ValidationResult; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.TreeMap; import java.util.function.Function; +import java.util.stream.Stream; public class Ortb2RequestFactory { @@ -99,7 +103,6 @@ public class Ortb2RequestFactory { private final ApplicationSettings applicationSettings; private final IpAddressHelper ipAddressHelper; private final HookStageExecutor hookStageExecutor; - private final PriceFloorProcessor priceFloorProcessor; private final CountryCodeMapper countryCodeMapper; private final Metrics metrics; @@ -115,7 +118,6 @@ public Ortb2RequestFactory(int timeoutAdjustmentFactor, ApplicationSettings applicationSettings, IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, - PriceFloorProcessor priceFloorProcessor, CountryCodeMapper countryCodeMapper, Metrics metrics) { @@ -135,7 +137,6 @@ public Ortb2RequestFactory(int timeoutAdjustmentFactor, this.applicationSettings = Objects.requireNonNull(applicationSettings); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); - this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); this.countryCodeMapper = Objects.requireNonNull(countryCodeMapper); this.metrics = Objects.requireNonNull(metrics); } @@ -206,6 +207,40 @@ public Future validateRequest(BidRequest bidRequest, : Future.succeededFuture(bidRequest); } + public BidRequest removeEmptyEids(BidRequest bidRequest, List warnings) { + final User user = bidRequest.getUser(); + + if (user == null) { + return bidRequest; + } + + final List eids = Stream.ofNullable(user.getEids()) + .flatMap(Collection::stream) + .map(eid -> eid.toBuilder().uids(removeEmptyUids(eid, warnings)).build()) + .filter(eid -> CollectionUtils.isNotEmpty(eid.getUids())) + .toList(); + + if (CollectionUtils.isEmpty(eids) && CollectionUtils.isNotEmpty(user.getEids())) { + warnings.add("removed empty EID array"); + } + + final User modifiedUser = user.toBuilder().eids(CollectionUtils.isEmpty(eids) ? null : eids).build(); + return bidRequest.toBuilder().user(modifiedUser).build(); + } + + private List removeEmptyUids(Eid eid, List warnings) { + return CollectionUtils.emptyIfNull(eid.getUids()).stream() + .filter(uid -> { + if (StringUtils.isBlank(uid.getId())) { + warnings.add("removed EID %s due to empty ID".formatted(eid.getSource())); + return false; + } + + return true; + }) + .toList(); + } + public Future enrichBidRequestWithGeolocationData(AuctionContext auctionContext) { final BidRequest bidRequest = auctionContext.getBidRequest(); final Device device = bidRequest.getDevice(); @@ -350,6 +385,7 @@ private static HttpRequestContext toHttpRequest(HookStageExecutionResult> fromRequest(RoutingContext routingC Endpoint.openrtb2_video, MetricName.video); return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) - .compose(httpRequest -> - createBidRequest(httpRequest) + .compose(httpRequest -> createBidRequest(httpRequest) + .map(bidRequest -> removeEmptyEids(bidRequest, initialAuctionContext.getDebugWarnings())) + .compose(bidRequest -> validateRequest( + bidRequest, + httpRequest, + initialAuctionContext.getDebugWarnings())) - .compose(bidRequest -> validateRequest( - bidRequest, - httpRequest, - initialAuctionContext.getDebugWarnings())) + .map(bidRequestWithErrors -> populatePodErrors( + bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - .map(bidRequestWithErrors -> populatePodErrors( - bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - - .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( - initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) + .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) .compose(auctionContext -> ortb2RequestFactory.fetchAccountWithoutStoredRequestLookup(auctionContext) .map(auctionContext::with)) @@ -154,6 +153,14 @@ public Future> fromRequest(RoutingContext routingC .map(auctionContext -> WithPodErrors.of(auctionContext, podErrors)); } + private WithPodErrors removeEmptyEids(WithPodErrors requestWithPodErrors, + List debugWarnings) { + + return WithPodErrors.of( + ortb2RequestFactory.removeEmptyEids(requestWithPodErrors.getData(), debugWarnings), + requestWithPodErrors.getPodErrors()); + } + private String extractAndValidateBody(RoutingContext routingContext) { final String body = routingContext.getBodyAsString(); if (body == null) { diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java new file mode 100644 index 00000000000..9ddeefb6e2e --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java @@ -0,0 +1,100 @@ +package org.prebid.server.bidadjustments; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class BidAdjustmentRulesValidator { + + public static final Set SUPPORTED_MEDIA_TYPES = Set.of( + BidAdjustmentsResolver.WILDCARD, + ImpMediaType.banner.toString(), + ImpMediaType.audio.toString(), + ImpMediaType.video_instream.toString(), + ImpMediaType.video_outstream.toString(), + ImpMediaType.xNative.toString()); + + private BidAdjustmentRulesValidator() { + + } + + public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException { + if (bidAdjustments == null) { + return; + } + + final Map>>> mediatypes = + bidAdjustments.getMediatype(); + + if (MapUtils.isEmpty(mediatypes)) { + return; + } + + for (String mediatype : mediatypes.keySet()) { + if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + if (MapUtils.isEmpty(bidders)) { + throw new ValidationException("no bidders found in %s".formatted(mediatype)); + } + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + + if (MapUtils.isEmpty(deals)) { + throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder)); + } + + for (String dealId : deals.keySet()) { + final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId); + validateRules(deals.get(dealId), path); + } + } + } + } + } + + private static void validateRules(List rules, + String path) throws ValidationException { + + if (rules == null) { + throw new ValidationException("no bid adjustment rules found in %s".formatted(path)); + } + + for (ExtRequestBidAdjustmentsRule rule : rules) { + final BidAdjustmentType type = rule.getAdjType(); + final String currency = rule.getCurrency(); + final BigDecimal value = rule.getValue(); + + final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency); + + final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN; + + final boolean invalidCpm = type == BidAdjustmentType.CPM + && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE)); + + final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER + && isValueNotInRange(value, 0, 100); + + final boolean invalidStatic = type == BidAdjustmentType.STATIC + && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE)); + + if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) { + throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path)); + } + } + } + + private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) { + return value == null + || value.compareTo(BigDecimal.valueOf(minValue)) < 0 + || value.compareTo(BigDecimal.valueOf(maxValue)) >= 0; + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java new file mode 100644 index 00000000000..1136876c7f6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -0,0 +1,205 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.ImpMediaTypeResolver; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.PbsUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class BidAdjustmentsProcessor { + + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; + + private final CurrencyConversionService currencyService; + private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final BidAdjustmentsResolver bidAdjustmentsResolver; + private final JacksonMapper mapper; + + public BidAdjustmentsProcessor(CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidAdjustmentsResolver bidAdjustmentsResolver, + JacksonMapper mapper) { + + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.bidAdjustmentsResolver = Objects.requireNonNull(bidAdjustmentsResolver); + this.mapper = Objects.requireNonNull(mapper); + } + + public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionParticipation, + BidRequest bidRequest, + BidAdjustments bidAdjustments) { + + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + + final List bidderBids = seatBid.getBids(); + if (bidderBids.isEmpty()) { + return auctionParticipation; + } + + final List errors = new ArrayList<>(seatBid.getErrors()); + final String bidder = auctionParticipation.getBidder(); + + final List updatedBidderBids = bidderBids.stream() + .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, bidAdjustments, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + final BidderResponse updatedBidderResponse = bidderResponse.with(seatBid.toBuilder() + .bids(updatedBidderBids) + .errors(errors) + .build()); + + return auctionParticipation.with(updatedBidderResponse); + } + + private BidderBid applyBidAdjustments(BidderBid bidderBid, + BidRequest bidRequest, + String bidder, + BidAdjustments bidAdjustments, + List errors) { + try { + final Price originalPrice = getOriginalPrice(bidderBid); + + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), + bidRequest.getImp(), + bidderBid.getType()); + + final Price priceWithFactorsApplied = applyBidAdjustmentFactors( + originalPrice, + bidder, + bidRequest, + mediaType); + + final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules( + priceWithFactorsApplied, + bidder, + bidRequest, + bidAdjustments, + mediaType, + bidderBid.getBid().getDealid()); + + return updateBid(originalPrice, priceWithAdjustmentsApplied, bidderBid, bidRequest); + } catch (PreBidException e) { + errors.add(BidderError.generic(e.getMessage())); + return null; + } + } + + private BidderBid updateBid(Price originalPrice, Price adjustedPrice, BidderBid bidderBid, BidRequest bidRequest) { + final Bid bid = bidderBid.getBid(); + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + final BigDecimal originalBidPrice = originalPrice.getValue(); + final String originalBidCurrency = originalPrice.getCurrency(); + updatedBidExt.set(ORIGINAL_BID_CPM, new DecimalNode(originalBidPrice)); + if (StringUtils.isNotBlank(originalBidCurrency)) { + updatedBidExt.set(ORIGINAL_BID_CURRENCY, new TextNode(originalBidCurrency)); + } + + final String requestCurrency = bidRequest.getCur().getFirst(); + final BigDecimal requestCurrencyPrice = currencyService.convertCurrency( + adjustedPrice.getValue(), + bidRequest, + adjustedPrice.getCurrency(), + requestCurrency); + + return bidderBid.toBuilder() + .bidCurrency(requestCurrency) + .bid(bid.toBuilder() + .ext(updatedBidExt) + .price(requestCurrencyPrice) + .build()) + .build(); + } + + private Price getOriginalPrice(BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + return Price.of(StringUtils.stripToNull(bidCurrency), price); + } + + private Price applyBidAdjustmentFactors(Price bidPrice, + String bidder, + BidRequest bidRequest, + ImpMediaType mediaType) { + + final String bidCurrency = bidPrice.getCurrency(); + final BigDecimal price = bidPrice.getValue(); + + final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidder, bidRequest, mediaType); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, price); + + return Price.of(bidCurrency, adjustedPrice.compareTo(price) != 0 ? adjustedPrice : price); + } + + private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, ImpMediaType mediaType) { + final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); + if (adjustmentFactors == null) { + return null; + } + + final ImpMediaType targetMediaType = mediaType == ImpMediaType.video_instream ? ImpMediaType.video : mediaType; + return bidAdjustmentFactorResolver.resolve(targetMediaType, adjustmentFactors, bidder); + } + + private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); + return prebid != null ? prebid.getBidadjustmentfactors() : null; + } + + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { + return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 + ? price.multiply(priceAdjustmentFactor) + : price; + } + + private Price applyBidAdjustmentRules(Price bidPrice, + String bidder, + BidRequest bidRequest, + BidAdjustments bidAdjustments, + ImpMediaType mediaType, + String dealId) { + + return bidAdjustmentsResolver.resolve( + bidPrice, + bidRequest, + bidAdjustments, + mediaType, + bidder, + dealId); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java new file mode 100644 index 00000000000..ffac1cbc51a --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java @@ -0,0 +1,106 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; +import org.prebid.server.util.dsl.config.PrebidConfigParameter; +import org.prebid.server.util.dsl.config.PrebidConfigParameters; +import org.prebid.server.util.dsl.config.PrebidConfigSource; +import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; +import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; +import org.prebid.server.util.dsl.config.impl.SimpleParameters; +import org.prebid.server.util.dsl.config.impl.SimpleSource; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class BidAdjustmentsResolver { + + public static final String WILDCARD = "*"; + public static final String DELIMITER = "|"; + + private final PrebidConfigMatchingStrategy matchingStrategy; + private final CurrencyConversionService currencyService; + + public BidAdjustmentsResolver(CurrencyConversionService currencyService) { + this.currencyService = Objects.requireNonNull(currencyService); + this.matchingStrategy = new MostAccurateCombinationStrategy(); + } + + public Price resolve(Price initialPrice, + BidRequest bidRequest, + BidAdjustments bidAdjustments, + ImpMediaType targetMediaType, + String targetBidder, + String targetDealId) { + + final List adjustmentsRules = findRules( + bidAdjustments, + targetMediaType, + targetBidder, + targetDealId); + + return adjustPrice(initialPrice, adjustmentsRules, bidRequest); + } + + private List findRules(BidAdjustments bidAdjustments, + ImpMediaType targetMediaType, + String targetBidder, + String targetDealId) { + + final Map> rules = bidAdjustments.getRules(); + final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); + final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId); + + final String rule = matchingStrategy.match(source, parameters); + return rule == null ? Collections.emptyList() : rules.get(rule); + } + + private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) { + final List conditionsMatchers = List.of( + SimpleDirectParameter.of(mediaType.toString()), + SimpleDirectParameter.of(bidder), + StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard()); + + return SimpleParameters.of(conditionsMatchers); + } + + private Price adjustPrice(Price price, + List bidAdjustmentRules, + BidRequest bidRequest) { + + String resolvedCurrency = price.getCurrency(); + BigDecimal resolvedPrice = price.getValue(); + + for (ExtRequestBidAdjustmentsRule rule : bidAdjustmentRules) { + final BidAdjustmentType adjustmentType = rule.getAdjType(); + final BigDecimal adjustmentValue = rule.getValue(); + final String adjustmentCurrency = rule.getCurrency(); + + switch (adjustmentType) { + case MULTIPLIER -> resolvedPrice = BidderUtil.roundFloor(resolvedPrice.multiply(adjustmentValue)); + case CPM -> { + final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency( + adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency); + resolvedPrice = BidderUtil.roundFloor(resolvedPrice.subtract(convertedAdjustmentValue)); + } + case STATIC -> { + resolvedPrice = adjustmentValue; + resolvedCurrency = adjustmentCurrency; + } + } + } + + return Price.of(resolvedCurrency, resolvedPrice); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java new file mode 100644 index 00000000000..6a151754bb2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java @@ -0,0 +1,86 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.validation.ValidationException; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsRetriever { + + private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsRetriever.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + private final double samplingRate; + + public BidAdjustmentsRetriever(JacksonMapper mapper, + JsonMerger jsonMerger, + double samplingRate) { + this.mapper = Objects.requireNonNull(mapper).mapper(); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.samplingRate = samplingRate; + } + + public BidAdjustments retrieve(AuctionContext auctionContext) { + final List debugWarnings = auctionContext.getDebugWarnings(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + + final JsonNode requestBidAdjustmentsNode = Optional.ofNullable(auctionContext.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode accountBidAdjustmentsNode = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidAdjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode mergedBidAdjustmentsNode = jsonMerger.merge( + requestBidAdjustmentsNode, + accountBidAdjustmentsNode); + + final List resolvedWarnings = debugEnabled ? debugWarnings : null; + return convertAndValidate(mergedBidAdjustmentsNode, resolvedWarnings, "request") + .or(() -> convertAndValidate(accountBidAdjustmentsNode, resolvedWarnings, "account")) + .orElse(BidAdjustments.of(Collections.emptyMap())); + } + + private Optional convertAndValidate(JsonNode bidAdjustmentsNode, + List debugWarnings, + String errorLocation) { + try { + final ExtRequestBidAdjustments accountBidAdjustments = mapper.convertValue( + bidAdjustmentsNode, + ExtRequestBidAdjustments.class); + + BidAdjustmentRulesValidator.validate(accountBidAdjustments); + return Optional.of(BidAdjustments.of(accountBidAdjustments)); + } catch (IllegalArgumentException | ValidationException e) { + final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); + if (debugWarnings != null) { + debugWarnings.add(message); + } + conditionalLogger.error(message, samplingRate); + return Optional.empty(); + } + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java new file mode 100644 index 00000000000..e9b790e5eab --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidadjustments.model; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum BidAdjustmentType { + + CPM, MULTIPLIER, STATIC, UNKNOWN; + + @SuppressWarnings("unused") + @JsonCreator + public static BidAdjustmentType of(String name) { + try { + return BidAdjustmentType.valueOf(name.toUpperCase()); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } + +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java new file mode 100644 index 00000000000..385a7644811 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java @@ -0,0 +1,52 @@ +package org.prebid.server.bidadjustments.model; + +import lombok.Value; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; +import org.prebid.server.bidadjustments.BidAdjustmentsResolver; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class BidAdjustments { + + private static final String RULE_SCHEME = + "%s" + BidAdjustmentsResolver.DELIMITER + "%s" + BidAdjustmentsResolver.DELIMITER + "%s"; + + Map> rules; + + public static BidAdjustments of(ExtRequestBidAdjustments bidAdjustments) { + if (bidAdjustments == null) { + return BidAdjustments.of(Collections.emptyMap()); + } + + final Map> rules = new HashMap<>(); + + final Map>>> mediatypes = + bidAdjustments.getMediatype(); + + if (MapUtils.isEmpty(mediatypes)) { + return BidAdjustments.of(Collections.emptyMap()); + } + + for (String mediatype : mediatypes.keySet()) { + if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + for (String dealId : deals.keySet()) { + rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); + } + } + } + } + + return BidAdjustments.of(MapUtils.unmodifiableMap(rules)); + } + +} diff --git a/src/main/java/org/prebid/server/bidder/BidderInfo.java b/src/main/java/org/prebid/server/bidder/BidderInfo.java index fffbb1bab5b..c9659135eb7 100644 --- a/src/main/java/org/prebid/server/bidder/BidderInfo.java +++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java @@ -28,6 +28,8 @@ public class BidderInfo { List vendors; + List currencyAccepted; + GdprInfo gdpr; boolean ccpaEnforced; @@ -49,6 +51,7 @@ public static BidderInfo create(boolean enabled, List doohMediaTypes, List supportedVendors, int vendorId, + List currencyAccepted, boolean ccpaEnforced, boolean modifyingVastXmlAllowed, CompressionType compressionType, @@ -66,6 +69,7 @@ public static BidderInfo create(boolean enabled, platformInfo(siteMediaTypes), platformInfo(doohMediaTypes)), supportedVendors, + currencyAccepted, new GdprInfo(vendorId), ccpaEnforced, modifyingVastXmlAllowed, diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 02d2632d6d6..0345aa4c29e 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -24,8 +24,9 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -62,24 +63,28 @@ public class HttpBidderRequester { private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private final HttpClient httpClient; private final BidderRequestCompletionTrackerFactory completionTrackerFactory; private final BidderErrorNotifier bidderErrorNotifier; private final HttpBidderRequestEnricher requestEnricher; private final JacksonMapper mapper; + private final double logSamplingRate; public HttpBidderRequester(HttpClient httpClient, BidderRequestCompletionTrackerFactory completionTrackerFactory, BidderErrorNotifier bidderErrorNotifier, HttpBidderRequestEnricher requestEnricher, - JacksonMapper mapper) { + JacksonMapper mapper, + double logSamplingRate) { this.httpClient = Objects.requireNonNull(httpClient); this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory); this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier); this.requestEnricher = Objects.requireNonNull(requestEnricher); this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; } /** @@ -100,7 +105,8 @@ public Future requestBids(Bidder bidder, final List errors = httpRequestsWithErrors.getErrors(); final List> httpRequests = enrichRequests( bidderName, httpRequestsWithErrors.getValue(), requestHeaders, aliases, bidRequest); - recordBidderProvidedErrors(bidRejectionTracker, errors); + + rejectErrors(bidRejectionTracker, errors, BidRejectionReason.REQUEST_BLOCKED_GENERAL); if (CollectionUtils.isEmpty(httpRequests)) { return emptyBidderSeatBidWithErrors(errors); @@ -144,11 +150,13 @@ private List> enrichRequests(String bidderName, .toList(); } - private static void recordBidderProvidedErrors(BidRejectionTracker rejectionTracker, List errors) { - errors.stream() + private static void rejectErrors(BidRejectionTracker bidRejectionTracker, + List bidderErrors, + BidRejectionReason reason) { + + bidderErrors.stream() .filter(error -> CollectionUtils.isNotEmpty(error.getImpIds())) - .forEach(error -> rejectionTracker.reject( - error.getImpIds(), BidRejectionReason.fromBidderError(error))); + .forEach(error -> bidRejectionTracker.rejectImps(error.getImpIds(), reason)); } private boolean isStoredResponse(List> httpRequests, String storedResponse, String bidder) { @@ -238,9 +246,9 @@ private static byte[] gzip(byte[] value) { /** * Produces {@link Future} with {@link BidderCall} containing request and error description. */ - private static Future> failResponse(Throwable exception, HttpRequest httpRequest) { - logger.warn("Error occurred while sending HTTP request to a bidder url: {} with message: {}", - httpRequest.getUri(), exception.getMessage()); + private Future> failResponse(Throwable exception, HttpRequest httpRequest) { + conditionalLogger.warn("Error occurred while sending HTTP request to a bidder url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); logger.debug("Error occurred while sending HTTP request to a bidder url: {}", exception, httpRequest.getUri()); @@ -374,16 +382,44 @@ private void handleBidderErrors(CompositeBidderResponse bidderResponse) { final List bidderErrors = bidderResponse != null ? bidderResponse.getErrors() : null; if (bidderErrors != null) { errorsRecorded.addAll(bidderErrors); - recordBidderProvidedErrors(bidRejectionTracker, bidderErrors); + rejectErrors(bidRejectionTracker, bidderErrors, BidRejectionReason.ERROR_GENERAL); } } private void handleBidderCallError(BidderCall bidderCall) { + final Set requestedImpIds = bidderCall.getRequest().getImpIds(); + if (CollectionUtils.isEmpty(requestedImpIds)) { + return; + } + + final Integer statusCode = Optional.ofNullable(bidderCall.getResponse()) + .map(HttpResponse::getStatusCode) + .orElse(null); + + if (statusCode != null && statusCode == HttpResponseStatus.SERVICE_UNAVAILABLE.code()) { + bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + return; + } + + if (statusCode != null + && (statusCode < HttpResponseStatus.OK.code() + || statusCode >= HttpResponseStatus.BAD_REQUEST.code())) { + + bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + return; + } + final BidderError callError = bidderCall.getError(); final BidderError.Type callErrorType = callError != null ? callError.getType() : null; - final Set requestedImpIds = bidderCall.getRequest().getImpIds(); - if (callErrorType != null && CollectionUtils.isNotEmpty(requestedImpIds)) { - bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.fromBidderError(callError)); + + if (callErrorType == null) { + return; + } + + if (callErrorType == BidderError.Type.timeout) { + bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_TIMED_OUT); + } else { + bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_GENERAL); } } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java index d6ab2363a52..672f31dd8af 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -19,6 +19,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; @@ -27,7 +28,9 @@ import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBidExt; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusNetBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusResponse; @@ -42,10 +45,12 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; @@ -208,7 +213,7 @@ private List> createHttpRequests(Map eids.getFirst()) + .map(List::getFirst) .map(Eid::getUids) .filter(CollectionUtils::isNotEmpty) - .map(uids -> uids.getFirst()) + .map(List::getFirst) .map(Uid::getId)) .map(AdnuntiusMetaData::of) .orElse(null); @@ -356,10 +361,10 @@ private List extractBids(BidRequest bidRequest, AdnuntiusResponse adn final String bidType = extImpAdnuntius.getBidType(); currency = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getCurrency); - bids.add(createBid(ad, adsUnit.getHtml(), impId, bidType)); + bids.add(createBid(ad, bidRequest, adsUnit.getHtml(), impId, bidType)); for (AdnuntiusAd deal : ListUtils.emptyIfNull(adsUnit.getDeals())) { - bids.add(createBid(deal, deal.getHtml(), impId, bidType)); + bids.add(createBid(deal, bidRequest, deal.getHtml(), impId, bidType)); } } @@ -374,8 +379,9 @@ private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { return CollectionUtils.isNotEmpty(ads) && ads.getFirst() != null; } - private static Bid createBid(AdnuntiusAd ad, String adm, String impId, String bidType) { + private Bid createBid(AdnuntiusAd ad, BidRequest bidRequest, String adm, String impId, String bidType) { final String adId = ad.getAdId(); + final AdnuntiusBidExt bidExt = prepareBidExt(ad, bidRequest); return Bid.builder() .id(adId) @@ -389,9 +395,32 @@ private static Bid createBid(AdnuntiusAd ad, String adm, String impId, String bi .price(resolvePrice(ad, bidType)) .adm(adm) .adomain(extractDomain(ad.getDestinationUrls())) + .ext(bidExt == null ? null : mapper.mapper().valueToTree(bidExt)) .build(); } + private static AdnuntiusBidExt prepareBidExt(AdnuntiusAd ad, BidRequest bidRequest) { + final ExtRegsDsa extRegsDsa = Optional.ofNullable(bidRequest.getRegs()) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .orElse(null); + + final AdnuntiusAdvertiser advertiser = ad.getAdvertiser(); + + if (advertiser != null && advertiser.getName() != null && extRegsDsa != null) { + final String legalName = ObjectUtils.firstNonNull(advertiser.getLegalName(), advertiser.getName()); + final ExtBidDsa dsa = ExtBidDsa.builder() + .adRender(0) + .paid(legalName) + .behalf(legalName) + .build(); + + return AdnuntiusBidExt.of(dsa); + } + + return null; + } + private static Integer parseMeasure(String measure) { try { return Integer.valueOf(measure); diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java index 47ff2c30f04..88367e172d6 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java @@ -40,4 +40,6 @@ public class AdnuntiusAd { @JsonProperty("destinationUrls") Map destinationUrls; + + AdnuntiusAdvertiser advertiser; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java new file mode 100644 index 00000000000..7c371a9b726 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AdnuntiusAdvertiser { + + @JsonProperty("legalName") + String legalName; + + String name; +} diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java new file mode 100644 index 00000000000..172a23471be --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; + +@Value(staticConstructor = "of") +public class AdnuntiusBidExt { + + ExtBidDsa dsa; +} diff --git a/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java b/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java index 606fc880894..76e716325f4 100644 --- a/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java +++ b/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java @@ -11,6 +11,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adtarget.proto.AdtargetImpExt; +import org.prebid.server.bidder.adtarget.proto.ExtImpAdtargetBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -67,16 +68,16 @@ private Result>> mapSourceIdToImp(List imps) { final Map> sourceToImps = new HashMap<>(); for (Imp imp : imps) { final ExtImpAdtarget extImpAdtarget; + final Integer sourceId; try { validateImpression(imp); extImpAdtarget = parseImpAdtarget(imp); + sourceId = resolveSourceId(imp.getId(), extImpAdtarget.getSourceId()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); continue; } - final Imp updatedImp = updateImp(imp, extImpAdtarget); - - final Integer sourceId = extImpAdtarget.getSourceId(); + final Imp updatedImp = updateImp(imp, sourceId, extImpAdtarget); sourceToImps.computeIfAbsent(sourceId, ignored -> new ArrayList<>()).add(updatedImp); } return Result.of(sourceToImps, errors); @@ -103,8 +104,9 @@ private static void validateImpression(Imp imp) { } } - private Imp updateImp(Imp imp, ExtImpAdtarget extImpAdtarget) { - final AdtargetImpExt adtargetImpExt = AdtargetImpExt.of(extImpAdtarget); + private Imp updateImp(Imp imp, Integer sourceId, ExtImpAdtarget extImpAdtarget) { + final AdtargetImpExt adtargetImpExt = AdtargetImpExt.of( + ExtImpAdtargetBidRequest.from(sourceId, extImpAdtarget)); final BigDecimal bidFloor = extImpAdtarget.getBidFloor(); return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) @@ -112,6 +114,14 @@ private Imp updateImp(Imp imp, ExtImpAdtarget extImpAdtarget) { .build(); } + private static Integer resolveSourceId(String impId, String sourceId) { + try { + return sourceId == null ? 0 : Integer.parseInt(sourceId); + } catch (NumberFormatException e) { + throw new PreBidException("ignoring imp id=%s, aid parsing err: %s".formatted(impId, e.getMessage())); + } + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { final List errors = new ArrayList<>(); diff --git a/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java b/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java index 4fc2177fbf0..3a2ec0278f4 100644 --- a/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java +++ b/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java @@ -1,14 +1,11 @@ package org.prebid.server.bidder.adtarget.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.proto.openrtb.ext.request.adtarget.ExtImpAdtarget; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdtargetImpExt { @JsonProperty("adtarget") - ExtImpAdtarget extImpAdtarget; + ExtImpAdtargetBidRequest extImp; } diff --git a/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java b/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java new file mode 100644 index 00000000000..0abfc880e74 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java @@ -0,0 +1,31 @@ +package org.prebid.server.bidder.adtarget.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.adtarget.ExtImpAdtarget; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpAdtargetBidRequest { + + @JsonProperty("aid") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; + + public static ExtImpAdtargetBidRequest from(Integer sourceId, ExtImpAdtarget impExt) { + return ExtImpAdtargetBidRequest.of( + sourceId, + impExt.getPlacementId(), + impExt.getSiteId(), + impExt.getBidFloor()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java b/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java index 1c88bed9005..404d83b9052 100644 --- a/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java +++ b/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java @@ -13,6 +13,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adtelligent.proto.AdtelligentImpExt; +import org.prebid.server.bidder.adtelligent.proto.ExtImpAdtelligentBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -86,16 +87,17 @@ private Result>> mapSourceIdToImp(List imps) { final Map> sourceToImps = new HashMap<>(); for (final Imp imp : imps) { final ExtImpAdtelligent extImpAdtelligent; + final Integer sourceId; try { validateImpression(imp); extImpAdtelligent = getExtImpAdtelligent(imp); + sourceId = resolveSourceId(imp.getId(), extImpAdtelligent.getSourceId()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); continue; } - final Imp updatedImp = updateImp(imp, extImpAdtelligent); + final Imp updatedImp = updateImp(imp, sourceId, extImpAdtelligent); - final Integer sourceId = extImpAdtelligent.getSourceId(); final List sourceIdImps = sourceToImps.get(sourceId); if (sourceIdImps == null) { sourceToImps.put(sourceId, new ArrayList<>(Collections.singleton(updatedImp))); @@ -164,8 +166,9 @@ private void validateImpression(Imp imp) { /** * Updates {@link Imp} with bidfloor if it is present in imp.ext.bidder */ - private Imp updateImp(Imp imp, ExtImpAdtelligent extImpAdtelligent) { - final AdtelligentImpExt adtelligentImpExt = AdtelligentImpExt.of(extImpAdtelligent); + private Imp updateImp(Imp imp, Integer sourceId, ExtImpAdtelligent extImpAdtelligent) { + final AdtelligentImpExt adtelligentImpExt = AdtelligentImpExt.of( + ExtImpAdtelligentBidRequest.from(sourceId, extImpAdtelligent)); final BigDecimal bidFloor = extImpAdtelligent.getBidFloor(); return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) @@ -173,6 +176,14 @@ private Imp updateImp(Imp imp, ExtImpAdtelligent extImpAdtelligent) { .build(); } + private static Integer resolveSourceId(String impId, String sourceId) { + try { + return sourceId == null ? 0 : Integer.parseInt(sourceId); + } catch (NumberFormatException e) { + throw new PreBidException("ignoring imp id=%s, aid parsing err: %s".formatted(impId, e.getMessage())); + } + } + /** * Extracts {@link Bid}s from response. */ diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java b/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java index ac23f7dd218..fc0bdd642a7 100644 --- a/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java +++ b/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java @@ -1,14 +1,11 @@ package org.prebid.server.bidder.adtelligent.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.proto.openrtb.ext.request.adtelligent.ExtImpAdtelligent; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdtelligentImpExt { @JsonProperty("adtelligent") - ExtImpAdtelligent extImpAdtelligent; + ExtImpAdtelligentBidRequest extImp; } diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java b/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java new file mode 100644 index 00000000000..e543e6a8e39 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java @@ -0,0 +1,31 @@ +package org.prebid.server.bidder.adtelligent.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.adtelligent.ExtImpAdtelligent; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpAdtelligentBidRequest { + + @JsonProperty("aid") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; + + public static ExtImpAdtelligentBidRequest from(Integer sourceId, ExtImpAdtelligent impExt) { + return ExtImpAdtelligentBidRequest.of( + sourceId, + impExt.getPlacementId(), + impExt.getSiteId(), + impExt.getBidFloor()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java new file mode 100644 index 00000000000..f780a020732 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java @@ -0,0 +1,145 @@ +package org.prebid.server.bidder.adtonos; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adtonos.ExtImpAdtonos; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class AdtonosBidder implements Bidder { + + private static final TypeReference> ADTONOS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{PublisherId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdtonosBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpAdtonos impExt = parseImpExt(bidRequest.getImp().getFirst()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(impExt), mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpAdtonos parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ADTONOS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext.bidder for impression index 0. Error Infomation: " + e.getMessage()); + } + } + + private String makeUrl(ExtImpAdtonos extImp) { + return endpointUrl.replace(PUBLISHER_ID_MACRO, extImp.getSupplierId()); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + final List bids = extractBids(bidResponse, httpCall.getRequest().getPayload(), errors); + + return Result.of(bids, errors); + } + + private static List extractBids(BidResponse bidResponse, + BidRequest bidRequest, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), bidRequest, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, BidRequest bidRequest, List errors) { + try { + return BidderBid.of(bid, resolveBidType(bid, bidRequest.getImp()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType resolveBidType(Bid bid, List imps) throws PreBidException { + final Integer markupType = bid.getMtype(); + if (markupType != null) { + switch (markupType) { + case 1 -> { + return BidType.banner; + } + case 2 -> { + return BidType.video; + } + case 3 -> { + return BidType.audio; + } + case 4 -> { + return BidType.xNative; + } + } + } + + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getVideo() != null) { + return BidType.video; + } + throw new PreBidException("Unsupported bidtype for bid: " + bid.getId()); + } + } + + throw new PreBidException("Failed to find impression: " + impId); + } +} diff --git a/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java new file mode 100644 index 00000000000..d425ebb9572 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bidmatic.ExtImpBidmatic; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class BidmaticBidder implements Bidder { + + private static final TypeReference> EXT_IMP_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BidmaticBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final Map> sourceToImpsMap = new HashMap<>(); + + for (Imp imp : request.getImp()) { + final ExtImpBidmatic extImp; + try { + extImp = parseImpExt(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + + final int sourceId; + try { + sourceId = Integer.parseInt(extImp.getSourceId()); + } catch (NumberFormatException e) { + errors.add(BidderError.badInput("Cannot parse sourceId=%s to int".formatted(extImp.getSourceId()))); + continue; + } + + final Imp modifiedImp = modifyImp(imp, sourceId, extImp); + sourceToImpsMap.putIfAbsent(sourceId, new ArrayList<>()); + sourceToImpsMap.get(sourceId).add(modifiedImp); + } + + if (sourceToImpsMap.isEmpty()) { + return Result.withErrors(errors); + } + + sourceToImpsMap.forEach((sourceId, imps) -> requests.add(makeHttpRequest(request, sourceId, imps))); + return Result.of(requests, errors); + } + + private ExtImpBidmatic parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_IMP_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, Integer sourceId, ExtImpBidmatic extImp) { + final BidmaticImpExt modifiedExtImp = BidmaticImpExt.of( + sourceId, extImp.getPlacementId(), extImp.getSiteId(), extImp.getBidFloor()); + + return imp.toBuilder() + .bidfloor(BidderUtil.isValidPrice(extImp.getBidFloor()) ? extImp.getBidFloor() : imp.getBidfloor()) + .ext(mapper.mapper().createObjectNode().set("bidmatic", mapper.mapper().valueToTree(modifiedExtImp))) + .build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, Integer sourceId, List imps) { + final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); + return BidderUtil.defaultRequest(modifiedRequest, makeUrl(sourceId), mapper); + } + + private String makeUrl(Integer sourceId) { + return endpointUrl + "?source=%d".formatted(sourceId); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final Map impMap = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, impMap, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, Map impMap, String currency, List errors) { + try { + final Pair bidType = getBidType(bid, impMap); + final Bid modifiedBid = bid.toBuilder().mtype(bidType.getRight()).build(); + return BidderBid.of(modifiedBid, bidType.getLeft(), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static Pair getBidType(Bid bid, Map impIdToImpMap) { + final Imp imp = impIdToImpMap.get(bid.getImpid()); + if (imp == null) { + throw new PreBidException("ignoring bid id=%s, request doesn't contain any impression with id=%s" + .formatted(bid.getId(), bid.getImpid())); + } + + if (imp.getBanner() != null) { + return Pair.of(BidType.banner, 1); + } else if (imp.getVideo() != null) { + return Pair.of(BidType.video, 2); + } else if (imp.getXNative() != null) { + return Pair.of(BidType.xNative, 4); + } else if (imp.getAudio() != null) { + return Pair.of(BidType.audio, 3); + } else { + return Pair.of(BidType.banner, 1); + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java new file mode 100644 index 00000000000..eead679d233 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java @@ -0,0 +1,22 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class BidmaticImpExt { + + @JsonProperty("source") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; +} diff --git a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java similarity index 80% rename from src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java rename to src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java index 2fc8185e301..c317935419b 100644 --- a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java +++ b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.bizzclick; +package org.prebid.server.bidder.blasto; import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; @@ -9,7 +9,6 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -21,7 +20,7 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.bizzclick.ExtImpBizzclick; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -29,13 +28,12 @@ import java.util.List; import java.util.Objects; -public class BizzclickBidder implements Bidder { +public class BlastoBidder implements Bidder { - private static final TypeReference> BIZZCLICK_EXT_TYPE_REFERENCE = + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String DEFAULT_HOST = "us-e-node1"; - private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String DEFAULT_CURRENCY = "USD"; @@ -43,7 +41,7 @@ public class BizzclickBidder implements Bidder { private final String endpointUrl; private final JacksonMapper mapper; - public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { + public BlastoBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -51,23 +49,23 @@ public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List imps = request.getImp(); - final ExtImpBizzclick extImpBizzclick; + final ExtImpBlasto extImp; try { - extImpBizzclick = parseImpExt(imps.getFirst()); + extImp = parseImpExt(imps.getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } final List modifiedImps = imps.stream() - .map(BizzclickBidder::modifyImp) + .map(BlastoBidder::modifyImp) .toList(); - return Result.withValue(createHttpRequest(request, modifiedImps, extImpBizzclick)); + return Result.withValue(createHttpRequest(request, modifiedImps, extImp)); } - private ExtImpBizzclick parseImpExt(Imp imp) throws PreBidException { + private ExtImpBlasto parseImpExt(Imp imp) throws PreBidException { try { - return mapper.mapper().convertValue(imp.getExt(), BIZZCLICK_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException("ext.bidder not provided"); } @@ -77,7 +75,7 @@ private static Imp modifyImp(Imp imp) { return imp.toBuilder().ext(null).build(); } - private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBizzclick ext) { + private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBlasto ext) { final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); return HttpRequest.builder() @@ -102,13 +100,10 @@ private static MultiMap headers(Device device) { return headers; } - private String buildEndpointUrl(ExtImpBizzclick ext) { - final String host = StringUtils.isBlank(ext.getHost()) ? DEFAULT_HOST : ext.getHost(); - final String sourceId = StringUtils.isBlank(ext.getSourceId()) ? ext.getPlacementId() : ext.getSourceId(); + private String buildEndpointUrl(ExtImpBlasto extImp) { return endpointUrl - .replace(URL_HOST_MACRO, HttpUtil.encodeUrl(host)) - .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(sourceId)) - .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(ext.getAccountId())); + .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId())) + .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId())); } @Override diff --git a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java index afac2b57f84..4c64992184b 100644 --- a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java +++ b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java @@ -91,18 +91,18 @@ private ExtImpConnectAd parseImpExt(Imp imp) { } catch (IllegalArgumentException e) { throw new PreBidException("Impression id=%s, has invalid Ext".formatted(imp.getId())); } - final Integer siteId = extImpConnectAd.getSiteId(); - if (siteId == null || siteId == 0) { + final String siteId = extImpConnectAd.getSiteId(); + if (siteId == null) { throw new PreBidException("Impression id=%s, has no siteId present".formatted(imp.getId())); } return extImpConnectAd; } - private Imp updateImp(Imp imp, Integer secure, Integer siteId, BigDecimal bidFloor) { + private Imp updateImp(Imp imp, Integer secure, String siteId, BigDecimal bidFloor) { final boolean isValidBidFloor = BidderUtil.isValidPrice(bidFloor); return imp.toBuilder() .banner(updateBanner(imp.getBanner())) - .tagid(siteId.toString()) + .tagid(siteId) .secure(secure) .bidfloor(isValidBidFloor ? bidFloor : imp.getBidfloor()) .bidfloorcur(isValidBidFloor ? "USD" : imp.getBidfloorcur()) diff --git a/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java b/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java new file mode 100644 index 00000000000..f8d739193a9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.copper6ssp; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.copper6ssp.proto.Copper6SspImpExtBidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.copper6ssp.ImpExtCopper6Ssp; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class Copper6SspBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public Copper6SspBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ImpExtCopper6Ssp extImp; + try { + extImp = parseImpExt(imp); + outgoingRequests.add(makeRequest(modifyImp(imp, extImp), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(outgoingRequests) + ? Result.withErrors(errors) + : Result.of(outgoingRequests, errors); + } + + private ImpExtCopper6Ssp parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ImpExtCopper6Ssp extImp) { + final Copper6SspImpExtBidder impExtBidder = getImpExtWithType(extImp); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(impExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private Copper6SspImpExtBidder getImpExtWithType(ImpExtCopper6Ssp impExtCopper6Ssp) { + final boolean hasPlacementId = StringUtils.isNotBlank(impExtCopper6Ssp.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(impExtCopper6Ssp.getEndpointId()); + + return Copper6SspImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? impExtCopper6Ssp.getPlacementId() : null) + .endpointId(hasEndpointId ? impExtCopper6Ssp.getEndpointId() : null) + .build(); + } + + private HttpRequest makeRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java b/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java new file mode 100644 index 00000000000..5feb52a144b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.copper6ssp.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class Copper6SspImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java new file mode 100644 index 00000000000..d6f43ef4c86 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java @@ -0,0 +1,27 @@ +package org.prebid.server.bidder.criteo; + +import com.iab.openrtb.response.SeatBid; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class CriteoBidResponse { + + String id; + + List seatbid; + + String bidid; + + String cur; + + String customdata; + + Integer nbr; + + CriteoExtBidResponse ext; + +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java index db9194ee215..064bac48c48 100644 --- a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; @@ -19,6 +19,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -30,6 +31,7 @@ public class CriteoBidder implements Bidder { + private static final String BIDDER_NAME = "criteo"; private final String endpointUrl; private final JacksonMapper mapper; @@ -44,16 +46,24 @@ public Result>> makeHttpRequests(BidRequest bidRequ } @Override + @Deprecated(forRemoval = true) public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + return Result.withError(BidderError.generic("Deprecated adapter method invoked")); + } + + @Override + public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBidsFromResponse(bidResponse)); + final CriteoBidResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), + CriteoBidResponse.class); + return CompositeBidderResponse.withBids(extractBids(bidResponse), extractFledge(bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBidsFromResponse(BidResponse bidResponse) { + private List extractBids(CriteoBidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -94,4 +104,22 @@ private ObjectNode makeExt(String networkName) { .meta(ExtBidPrebidMeta.builder().networkName(networkName).build()) .build()); } + + private static List extractFledge(CriteoBidResponse bidResponse) { + final List fledgeConfigs = Optional.ofNullable(bidResponse) + .map(CriteoBidResponse::getExt) + .map(CriteoExtBidResponse::getIgi) + .filter(CollectionUtils::isNotEmpty) + .orElse(Collections.emptyList()) + .stream() + .filter(igi -> CollectionUtils.isNotEmpty(igi.getIgs()) && igi.getIgs().getFirst() != null) + .map(igi -> FledgeAuctionConfig.builder() + .impId(igi.getImpId()) + .bidder(BIDDER_NAME) + .config(igi.getIgs().getFirst().getConfig()) + .build()) + .toList(); + + return CollectionUtils.isEmpty(fledgeConfigs) ? null : fledgeConfigs; + } } diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java new file mode 100644 index 00000000000..8f332b2f3bd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoExtBidResponse { + + List igi; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java new file mode 100644 index 00000000000..6bdac80ad2f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoIgiExtBidResponse { + + String impId; + + List igs; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java new file mode 100644 index 00000000000..b34d76e0646 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.criteo; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class CriteoIgsIgiExtBidResponse { + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java index b0207293e4d..fe0e8ad6a03 100644 --- a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java +++ b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java @@ -102,11 +102,8 @@ private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { final BigDecimal bidFloor = imp.getBidfloor(); final String bidFloorCurrency = imp.getBidfloorcur(); - if (!BidderUtil.isValidPrice(bidFloor)) { - throw new PreBidException("BidFloor should be defined"); - } - - if (StringUtils.isNotBlank(bidFloorCurrency) + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); } diff --git a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java index 25578dafb75..77ec518e216 100644 --- a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java +++ b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java @@ -262,7 +262,13 @@ private Bid updateBidWithId(Bid bid) { private static BidType getType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { - return imp.getVideo() != null ? BidType.video : BidType.banner; + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getAudio() != null) { + return BidType.audio; + } else { + return BidType.banner; + } } } return BidType.banner; diff --git a/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java b/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java new file mode 100644 index 00000000000..6d520a2ecd0 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java @@ -0,0 +1,140 @@ +package org.prebid.server.bidder.escalax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.escalax.ExtImpEscalax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class EscalaxBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String X_OPENRTB_VERSION = "2.5"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public EscalaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpEscalax extImp; + try { + extImp = parseImpExt(firstImp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + return Result.withValue(makeHttpRequest(createRequest(request), extImp)); + } + + private static BidRequest createRequest(BidRequest request) { + return request.toBuilder().imp(prepareFirstImp(request.getImp())).build(); + } + + private static List prepareFirstImp(List imps) { + final Imp firstImp = imps.getFirst(); + final List updatedImps = new ArrayList<>(imps); + updatedImps.set(0, firstImp.toBuilder().ext(null).build()); + + return updatedImps; + } + + private HttpRequest makeHttpRequest(BidRequest bidRequest, ExtImpEscalax extImp) { + return BidderUtil.defaultRequest(bidRequest, makeHeaders(bidRequest.getDevice()), makeUrl(extImp), mapper); + } + + private String makeUrl(ExtImpEscalax extImp) { + return endpointUrl + .replace("{{AccountID}}", extImp.getAccountId()) + .replace("{{SourceId}}", extImp.getSourceId()); + } + + private MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + headers.set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, + ObjectUtil.getIfNotNull(device, Device::getUa)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIpv6)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIp)); + + return headers; + } + + private ExtImpEscalax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing escalaxExt - " + e.getMessage()); + } + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unsupported MType " + mtype); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java index e444038f329..62b0ad34155 100644 --- a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java @@ -34,7 +34,7 @@ public class GothamAdsBidder implements Bidder { private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; - private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; + private static final String ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String X_OPENRTB_VERSION = "2.5"; private final String endpointUrl; diff --git a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java index 783caad5d1a..cecb9e3c473 100644 --- a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java +++ b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java @@ -135,7 +135,6 @@ private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { final Video video = imp.getVideo(); if (video != null) { - validateVideoParams(video); final String irisId = extImp.getIrisId(); if (StringUtils.isNotEmpty(irisId)) { final Video resolvedVideo = resolveVideo(video, irisId); @@ -174,18 +173,6 @@ private static ExtImpGumgumBanner resolveBannerExt(List formats, Long sl .orElseGet(() -> ExtImpGumgumBanner.of(slot, 0, 0)); } - private void validateVideoParams(Video video) { - if (anyOfNull( - video.getW(), - video.getH(), - video.getMinduration(), - video.getMaxduration(), - video.getPlacement(), - video.getLinearity())) { - throw new PreBidException("Invalid or missing video field(s)"); - } - } - private Video resolveVideo(Video video, String irisId) { final ObjectNode videoExt = mapper.mapper().valueToTree(ExtImpGumgumVideo.of(irisId)); return video.toBuilder().ext(videoExt).build(); diff --git a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java index a2cba1d3c36..e86a6182eb9 100644 --- a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java @@ -4,11 +4,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -26,13 +24,10 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.improvedigital.ExtImpImprovedigital; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; import java.util.Collection; @@ -50,9 +45,6 @@ public class ImprovedigitalBidder implements Bidder { private static final TypeReference> IMPROVEDIGITAL_EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String CONSENT_PROVIDERS_SETTINGS_OUT_KEY = "consented_providers_settings"; - private static final String CONSENTED_PROVIDERS_KEY = "consented_providers"; - private static final String REGEX_SPLIT_STRING_BY_DOT = "\\."; private static final String IS_REWARDED_INVENTORY_FIELD = "is_rewarded_inventory"; private static final JsonPointer IS_REWARDED_INVENTORY_POINTER @@ -89,46 +81,6 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValues(httpRequests); } - private ExtUser getAdditionalConsentProvidersUserExt(ExtUser extUser) { - final String consentedProviders = ObjectUtil.getIfNotNull( - ObjectUtil.getIfNotNull(extUser, ExtUser::getConsentedProvidersSettings), - ConsentedProvidersSettings::getConsentedProviders); - - if (StringUtils.isBlank(consentedProviders)) { - return extUser; - } - - final String[] consentedProvidersParts = StringUtils.split(consentedProviders, "~"); - final String consentedProvidersPart = consentedProvidersParts.length > 1 ? consentedProvidersParts[1] : null; - if (StringUtils.isBlank(consentedProvidersPart)) { - return extUser; - } - - return fillExtUser(extUser, consentedProvidersPart.split(REGEX_SPLIT_STRING_BY_DOT)); - } - - private ExtUser fillExtUser(ExtUser extUser, String[] arrayOfSplitString) { - final JsonNode consentProviderSettingJsonNode; - try { - consentProviderSettingJsonNode = customJsonNode(arrayOfSplitString); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - - return mapper.fillExtension(extUser, consentProviderSettingJsonNode); - } - - private JsonNode customJsonNode(String[] arrayOfSplitString) { - final Integer[] integers = mapper.mapper().convertValue(arrayOfSplitString, Integer[].class); - final ArrayNode arrayNode = mapper.mapper().createArrayNode(); - for (Integer integer : integers) { - arrayNode.add(integer); - } - - return mapper.mapper().createObjectNode().set(CONSENT_PROVIDERS_SETTINGS_OUT_KEY, - mapper.mapper().createObjectNode().set(CONSENTED_PROVIDERS_KEY, arrayNode)); - } - private ExtImpImprovedigital parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), IMPROVEDIGITAL_EXT_TYPE_REFERENCE).getBidder(); @@ -149,12 +101,8 @@ private static Imp updateImp(Imp imp) { } private HttpRequest resolveRequest(BidRequest bidRequest, Imp imp, Integer publisherId) { - final User user = bidRequest.getUser(); final BidRequest modifiedRequest = bidRequest.toBuilder() .imp(Collections.singletonList(updateImp(imp))) - .user(user != null - ? user.toBuilder().ext(getAdditionalConsentProvidersUserExt(user.getExt())).build() - : null) .build(); final String pathPrefix = publisherId != null && publisherId > 0 diff --git a/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java b/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java index 4a32a504fb8..33e8d25e9d7 100644 --- a/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; @@ -95,40 +96,37 @@ private Imp updateImp(Imp imp) { public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.of(extractBids(bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); + return bidsFromResponse(bidResponse); } - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private List bidsFromResponse(BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) .toList(); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - } - } - return BidType.banner; + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unsupported mtype %d for bid %s" + .formatted(mtype, bid.getId())); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java index 5c7c468af8f..5fb26e698fd 100644 --- a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java @@ -409,11 +409,14 @@ private static ExtBidPrebidVideo videoInfo(ExtBidPrebidVideo extBidPrebidVideo) private List extractFledge(IxBidResponse bidResponse) { return Optional.ofNullable(bidResponse) .map(IxBidResponse::getExt) - .map(IxExtBidResponse::getFledgeAuctionConfigs) - .orElse(Collections.emptyMap()) - .entrySet() + .map(IxExtBidResponse::getProtectedAudienceAuctionConfigs) + .orElse(Collections.emptyList()) .stream() - .map(e -> FledgeAuctionConfig.builder().impId(e.getKey()).config(e.getValue()).build()) + .filter(Objects::nonNull) + .map(ixAuctionConfig -> FledgeAuctionConfig.builder() + .impId(ixAuctionConfig.getBidId()) + .config(ixAuctionConfig.getConfig()) + .build()) .toList(); } } diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java new file mode 100644 index 00000000000..709fab87429 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java @@ -0,0 +1,14 @@ +package org.prebid.server.bidder.ix.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AuctionConfigExtBidResponse { + + @JsonProperty("bidId") + String bidId; + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java index c292317d22c..c586817df2c 100644 --- a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java @@ -1,15 +1,14 @@ package org.prebid.server.bidder.ix.model.response; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; -import java.util.Map; +import java.util.List; @Value(staticConstructor = "of") public class IxExtBidResponse { - @JsonProperty("fledge_auction_configs") - Map fledgeAuctionConfigs; + @JsonProperty("protectedAudienceAuctionConfigs") + List protectedAudienceAuctionConfigs; } diff --git a/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java b/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java deleted file mode 100644 index 541867ad71a..00000000000 --- a/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.bidder.liftoff.model; - -import lombok.Builder; -import lombok.Getter; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; - -@Builder(toBuilder = true) -@Getter -public class LiftoffImpressionExt { - - ExtImpPrebid prebid; - - ExtImpLiftoff bidder; - - ExtImpLiftoff vungle; -} diff --git a/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java b/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java new file mode 100644 index 00000000000..3774ef5e060 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java @@ -0,0 +1,95 @@ +package org.prebid.server.bidder.loopme; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class LoopmeBidder implements Bidder { + + private final String endpointUrl; + + private final JacksonMapper mapper; + + public LoopmeBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(request)) + .body(mapper.encodeToBytes(request)) + .payload(request) + .build()); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java b/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java new file mode 100644 index 00000000000..226a9a60149 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java @@ -0,0 +1,211 @@ +package org.prebid.server.bidder.melozen; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.melozen.MeloZenImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MeloZenBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String BIDDER_CURRENCY = "USD"; + private static final String EXT_PREBID = "prebid"; + + private final CurrencyConversionService currencyConversionService; + private final String endpointUrl; + private final JacksonMapper mapper; + + public MeloZenBidder(CurrencyConversionService currencyConversionService, + String endpoint, + JacksonMapper mapper) { + + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final MeloZenImpExt impExt = parseImpExt(imp); + final String url = resolveEndpoint(impExt); + final Imp modifiedImp = modifyImp(request, imp); + splitImpByMediaType(modifiedImp).forEach(splitImp -> + requests.add(BidderUtil.defaultRequest(modifyRequest(request, splitImp), url, mapper))); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private MeloZenImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp) { + final Price resolvedFloor = resolveBidFloor(bidRequest, imp); + return imp.toBuilder() + .bidfloor(resolvedFloor.getValue()) + .bidfloorcur(resolvedFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + bidFloor, + bidRequest, + bidFloorCurrency, + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedFloor); + } + + return Price.of(bidFloorCurrency, bidFloor); + } + + private String resolveEndpoint(MeloZenImpExt impExt) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getPubId()))); + } + + private List splitImpByMediaType(Imp imp) { + final Banner banner = imp.getBanner(); + final Video video = imp.getVideo(); + final Native xNative = imp.getXNative(); + + if (ObjectUtils.allNull(banner, video, xNative)) { + throw new PreBidException("Invalid MediaType. MeloZen only supports Banner, Video and Native."); + } + + final List imps = new ArrayList<>(); + + if (banner != null) { + imps.add(imp.toBuilder().video(null).xNative(null).build()); + } + + if (video != null) { + imps.add(imp.toBuilder().banner(null).xNative(null).build()); + } + + if (xNative != null) { + imps.add(imp.toBuilder().banner(null).video(null).build()); + } + + return imps; + } + + private BidRequest modifyRequest(BidRequest request, Imp imp) { + return request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> toBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid toBidderBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(() -> new PreBidException( + "Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid()))); + } + + private ExtBidPrebid parseExtBidPrebid(ObjectNode prebid) { + try { + return mapper.mapper().treeToValue(prebid, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java new file mode 100644 index 00000000000..08b7aec984e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java @@ -0,0 +1,158 @@ +package org.prebid.server.bidder.metax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.metax.ExtImpMetax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MetaxBidder implements Bidder { + + private static final TypeReference> METAX_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{publisherId}}"; + private static final String AD_UNIT_MACRO = "{{adUnit}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MetaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMetax extImpMetax = parseImpExt(imp); + httpRequests.add(BidderUtil.defaultRequest(prepareBidRequest(request, imp), + resolveEndpoint(extImpMetax), + mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpMetax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), METAX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static BidRequest prepareBidRequest(BidRequest bidRequest, Imp imp) { + return bidRequest.toBuilder() + .imp(Collections.singletonList(modifyImp(imp))) + .build(); + } + + private static Imp modifyImp(Imp imp) { + final Banner banner = imp.getBanner(); + final Integer width = banner != null ? banner.getW() : null; + final Integer height = banner != null ? banner.getH() : null; + if (width != null && height != null) { + return imp; + } + + final List formats = banner != null ? banner.getFormat() : null; + if (CollectionUtils.isEmpty(formats)) { + return imp; + } + + final Format firstFormat = formats.getFirst(); + return imp.toBuilder() + .banner(banner.toBuilder() + .w(Optional.ofNullable(firstFormat).map(Format::getW).orElse(0)) + .h(Optional.ofNullable(firstFormat).map(Format::getH).orElse(0)) + .build()) + .build(); + } + + private String resolveEndpoint(ExtImpMetax extImpMetax) { + final String publisherIdAsString = Optional.ofNullable(extImpMetax.getPublisherId()) + .map(Object::toString) + .orElse(StringUtils.EMPTY); + final String adUnitAsString = Optional.ofNullable(extImpMetax.getAdUnit()) + .map(Object::toString) + .orElse(StringUtils.EMPTY); + + return endpointUrl + .replace(PUBLISHER_ID_MACRO, publisherIdAsString) + .replace(AD_UNIT_MACRO, adUnitAsString); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unsupported MType: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java new file mode 100644 index 00000000000..80bcba81fc6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -0,0 +1,25 @@ +package org.prebid.server.bidder.missena; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class MissenaAdRequest { + + String requestId; + + int timeout; + + String referer; + + String refererCanonical; + + String consentString; + + boolean consentRequired; + + String placement; + + String test; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java new file mode 100644 index 00000000000..6a34c31efbf --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java @@ -0,0 +1,21 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Builder +@Value +public class MissenaAdResponse { + + String ad; + + BigDecimal cpm; + + String currency; + + @JsonProperty("requestId") + String requestId; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java new file mode 100644 index 00000000000..913fd3b44b8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -0,0 +1,157 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MissenaBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final int AD_REQUEST_DEFAULT_TIMEOUT = 2000; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MissenaBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMissena extImp = parseImpExt(imp); + requests.add(makeHttpRequest(request, imp.getId(), extImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private ExtImpMissena parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing missenaExt parameters"); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, String impId, ExtImpMissena extImp) { + final Site site = request.getSite(); + + final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() + .requestId(request.getId()) + .timeout(AD_REQUEST_DEFAULT_TIMEOUT) + .referer(site == null ? null : site.getPage()) + .refererCanonical(site == null ? null : site.getDomain()) + .consentString(getUserConsent(request.getUser())) + .consentRequired(isGdpr(request.getRegs())) + .placement(extImp.getPlacement()) + .test(extImp.getTestMode()) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImp.getApiKey())) + .headers(makeHeaders(request.getDevice(), site)) + .impIds(Collections.singleton(impId)) + .body(mapper.encodeToBytes(missenaAdRequest)) + .payload(missenaAdRequest) + .build(); + } + + private MultiMap makeHeaders(Device device, Site site) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + } + + return headers; + } + + private String makeUrl(String apiKey) { + return endpointUrl + "?t=%s".formatted(apiKey); + } + + private static boolean isGdpr(Regs regs) { + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .map(gdpr -> gdpr == 1) + .orElse(false); + } + + private static String getUserConsent(User user) { + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(StringUtils.EMPTY); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final MissenaAdResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), + MissenaAdResponse.class); + return Result.withValues(Collections.singletonList(extractBid(bidRequest, bidResponse))); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private BidderBid extractBid(BidRequest request, MissenaAdResponse response) { + final Bid bid = Bid.builder() + .id(request.getId()) + .price(response.getCpm()) + .impid(request.getImp().getFirst().getId()) + .adm(response.getAd()) + .crid(response.getRequestId()) + .build(); + + return BidderBid.of(bid, BidType.banner, response.getCurrency()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/model/BidderError.java b/src/main/java/org/prebid/server/bidder/model/BidderError.java index bf6fa169f7e..0873570a6d3 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderError.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderError.java @@ -94,7 +94,6 @@ public enum Type { * Covers the case where a bid was rejected by price-floors feature functionality */ rejected_ipf(6), - invalid_creative(350), timeout(1), generic(999); diff --git a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java index fec1f7d2ddf..2ef79d3bfd4 100644 --- a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java +++ b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java @@ -29,6 +29,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.openx.ExtImpOpenx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -253,10 +254,26 @@ private static List bidsFromResponse(BidRequest bidRequest, OpenxBidR .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid, impIdToBidType), bidCurrency)) + .map(bid -> toBidderBid(bid, impIdToBidType, bidCurrency)) .toList(); } + private static BidderBid toBidderBid(Bid bid, Map impIdToBidType, String bidCurrency) { + final BidType bidType = getBidType(bid, impIdToBidType); + final ExtBidPrebidVideo videoInfo = bidType == BidType.video ? getVideoInfo(bid) : null; + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(bidCurrency) + .videoInfo(videoInfo) + .build(); + } + + private static ExtBidPrebidVideo getVideoInfo(Bid bid) { + final String primaryCategory = CollectionUtils.isEmpty(bid.getCat()) ? null : bid.getCat().getFirst(); + return ExtBidPrebidVideo.of(bid.getDur(), primaryCategory); + } + private static Map impIdToBidType(BidRequest bidRequest) { return bidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, imp -> imp.getBanner() != null ? BidType.banner : BidType.video)); diff --git a/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java new file mode 100644 index 00000000000..b7e9ff64afa --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.oraki; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.oraki.ExtImpOraki; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class OrakiBidder implements Bidder { + + private static final TypeReference> ORAKI_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OrakiBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpOraki extImpOraki; + try { + extImpOraki = parseImpExt(imp); + outgoingRequests.add(createSingleRequest(modifyImp(imp, extImpOraki), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(outgoingRequests, errors); + } + + private ExtImpOraki parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ORAKI_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpOraki extImpOraki) { + final OrakiImpExtBidder orakiImpExtBidder = getImpExtOrakiWithType(extImpOraki); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(orakiImpExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private OrakiImpExtBidder getImpExtOrakiWithType(ExtImpOraki extImpOraki) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpOraki.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpOraki.getEndpointId()); + + return OrakiImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpOraki.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpOraki.getEndpointId() : null) + .build(); + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} + diff --git a/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java b/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java new file mode 100644 index 00000000000..4c3e8c3b9f5 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.oraki.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class OrakiImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java new file mode 100644 index 00000000000..59258985635 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java @@ -0,0 +1,142 @@ +package org.prebid.server.bidder.ownadx; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ownadx.ExtImpOwnAdx; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class OwnAdxBidder implements Bidder { + + private static final TypeReference> OWN_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String X_OPEN_RTB_VERSION = "2.5"; + private static final String SEAT_ID_MACROS_ENDPOINT = "{{SeatID}}"; + private static final String SSP_ID_MACROS_ENDPOINT = "{{SspID}}"; + private static final String TOKEN_ID_MACROS_ENDPOINT = "{{TokenID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OwnAdxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + for (Imp imp : bidRequest.getImp()) { + try { + final ExtImpOwnAdx impOwnAdx = parseImpExt(imp); + httpRequests.add(createHttpRequest(bidRequest, impOwnAdx)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpOwnAdx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), OWN_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private HttpRequest createHttpRequest(BidRequest bidRequest, ExtImpOwnAdx extImpOwnAdx) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImpOwnAdx)) + .headers(makeHeaders()) + .body(mapper.encodeToBytes(bidRequest)) + .impIds(BidderUtil.impIds(bidRequest)) + .payload(bidRequest) + .build(); + } + + private String makeUrl(ExtImpOwnAdx extImpOwnAdx) { + final Optional ownAdx = Optional.ofNullable(extImpOwnAdx); + return endpointUrl + .replace(SEAT_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getSeatId).orElse(StringUtils.EMPTY)) + .replace(SSP_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getSspId).orElse(StringUtils.EMPTY)) + .replace(TOKEN_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getTokenId).orElse(StringUtils.EMPTY)); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPEN_RTB_VERSION); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidMediaType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType " + bid.getMtype()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java index ce0a5161120..a73438b3ce3 100644 --- a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java +++ b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java @@ -14,15 +14,19 @@ import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.pgamssp.PgamSspImpExt; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,12 +39,18 @@ public class PgamSspBidder implements Bidder { }; private static final String PUBLISHER_IMP_EXT_TYPE = "publisher"; private static final String NETWORK_IMP_EXT_TYPE = "network"; + private static final String DEFAULT_BID_CURRENCY = "USD"; private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public PgamSspBidder(String endpointUrl, JacksonMapper mapper) { + public PgamSspBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -51,7 +61,7 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { final PgamSspImpExt impExt = parseImpExt(imp); - final BidRequest modifiedBidRequest = makeRequest(request, imp, impExt); + final BidRequest modifiedBidRequest = makeRequest(request, modifyImp(imp, request), impExt); httpRequests.add(makeHttpRequest(modifiedBidRequest, imp.getId())); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -61,6 +71,31 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValues(httpRequests); } + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + final Price resolvedBidFloor = resolveBidFloor(imp, bidRequest); + return imp.toBuilder() + .bidfloor(resolvedBidFloor.getValue()) + .bidfloorcur(resolvedBidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + + return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); + } + private PgamSspImpExt parseImpExt(Imp imp) throws PreBidException { try { return mapper.mapper().convertValue(imp.getExt(), PGAMSSP_EXT_TYPE_REFERENCE).getBidder(); diff --git a/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java b/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java new file mode 100644 index 00000000000..f8fe37197b0 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.pubrise; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.pubrise.proto.PubriseImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.pubrise.ExtImpPubrise; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class PubriseBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public PubriseBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpPubrise extImp; + try { + extImp = parseImpExt(imp); + outgoingRequests.add(makeRequest(modifyImp(imp, extImp), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(outgoingRequests) + ? Result.withError(BidderError.badInput("found no valid impressions")) + : Result.of(outgoingRequests, errors); + } + + private ExtImpPubrise parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpPubrise extImp) { + final PubriseImpExtBidder impExtBidder = getImpExtWithType(extImp); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(impExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private PubriseImpExtBidder getImpExtWithType(ExtImpPubrise extImpQt) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpQt.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpQt.getEndpointId()); + + return PubriseImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpQt.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpQt.getEndpointId() : null) + .build(); + } + + private HttpRequest makeRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java b/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java new file mode 100644 index 00000000000..2cb89d4d287 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.pubrise.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class PubriseImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/qt/QtBidder.java b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java new file mode 100644 index 00000000000..4b07ff86e97 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java @@ -0,0 +1,137 @@ +package org.prebid.server.bidder.qt; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.qt.ExtImpQt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class QtBidder implements Bidder { + + private static final TypeReference> QT_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public QtBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpQt extImpQt; + try { + extImpQt = parseImpExt(imp); + outgoingRequests.add(createSingleRequest(modifyImp(imp, extImpQt), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(outgoingRequests, errors); + } + + private ExtImpQt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), QT_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpQt extImpQt) { + final QtImpExtBidder qtImpExtBidder = getImpExtQtWithType(extImpQt); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(qtImpExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private QtImpExtBidder getImpExtQtWithType(ExtImpQt extImpQt) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpQt.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpQt.getEndpointId()); + + return QtImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpQt.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpQt.getEndpointId() : null) + .build(); + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java b/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java new file mode 100644 index 00000000000..4f9abd708c4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.qt.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class QtImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java index 91a8b5b09c4..39c269b163b 100644 --- a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java +++ b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java @@ -41,6 +41,7 @@ public class RtbhouseBidder implements Bidder { new TypeReference<>() { }; private static final String BIDDER_CURRENCY = "USD"; + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; private final String endpointUrl; private final JacksonMapper mapper; @@ -127,7 +128,7 @@ private BidderBid resolveBidderBid(Bid bid, .build(); return BidderBid.builder() - .bid(updatedBid) + .bid(resolveMacros(updatedBid)) .type(bidType) .bidCurrency(currency) .build(); @@ -212,4 +213,14 @@ private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidR } } + private static Bid resolveMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + return bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .build(); + } + } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 931de1bf1fa..a420efed87d 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -110,6 +110,7 @@ import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ListUtil; import org.prebid.server.util.ObjectUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.net.URISyntaxException; @@ -154,6 +155,9 @@ public class RubiconBidder implements Bidder { private static final String DFP_ADUNIT_CODE_FIELD = "dfp_ad_unit_code"; private static final String STYPE_FIELD = "stype"; private static final String PREBID_EXT = "prebid"; + private static final String PBS_LOGIN = "pbs_login"; + private static final String PBS_VERSION = "pbs_version"; + private static final String PBS_URL = "pbs_url"; private static final String PPUID_STYPE = "ppuid"; private static final String SHA256EMAIL_STYPE = "sha256email"; @@ -180,33 +184,38 @@ public class RubiconBidder implements Bidder { private final String bidderName; private final String endpointUrl; + private final String externalUrl; + private final String xapiUsername; private final Set supportedVendors; private final boolean generateBidId; - private final boolean useVideoSizeLogic; private final CurrencyConversionService currencyConversionService; private final PriceFloorResolver floorResolver; + private final PrebidVersionProvider versionProvider; private final JacksonMapper mapper; private final MultiMap headers; public RubiconBidder(String bidderName, String endpoint, + String externalUrl, String xapiUsername, String xapiPassword, List supportedVendors, boolean generateBidId, - boolean useVideoSizeLogic, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, JacksonMapper mapper) { this.bidderName = Objects.requireNonNull(bidderName); this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.externalUrl = HttpUtil.validateUrl(Objects.requireNonNull(externalUrl)); + this.xapiUsername = Objects.requireNonNull(xapiUsername); this.supportedVendors = Set.copyOf(Objects.requireNonNull(supportedVendors)); this.generateBidId = generateBidId; - this.useVideoSizeLogic = useVideoSizeLogic; this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.floorResolver = Objects.requireNonNull(floorResolver); + this.versionProvider = Objects.requireNonNull(versionProvider); this.mapper = Objects.requireNonNull(mapper); headers = headers(Objects.requireNonNull(xapiUsername), Objects.requireNonNull(xapiPassword)); @@ -606,7 +615,7 @@ private BigDecimal convertToXAPICurrency(BigDecimal value, private static BigDecimal resolveBidFloorPrice(Imp imp) { final BigDecimal bidFloor = imp.getBidfloor(); - return BidderUtil.isValidPrice(bidFloor) ? bidFloor : null; + return bidFloor != null && bidFloor.compareTo(BigDecimal.ZERO) >= 0 ? bidFloor : null; } private static String resolveBidFloorCurrency(Imp imp, BidRequest bidRequest, List errors) { @@ -704,7 +713,11 @@ private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App mergeFirstPartyDataFromApp(app, result); mergeFirstPartyDataFromImp(imp, rubiconImpExt, result); - return !result.isEmpty() ? result : null; + result.put(PBS_LOGIN, xapiUsername); + result.put(PBS_VERSION, versionProvider.getNameVersionRecord()); + result.put(PBS_URL, externalUrl); + + return result; } private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult, @@ -1011,9 +1024,8 @@ private Video makeVideo(Imp imp, RubiconVideoParams rubiconVideoParams, String r private Integer resolveSizeId(RubiconVideoParams rubiconVideoParams, Imp imp, String referer) { final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; - final Video video = imp.getVideo(); final Integer resolvedSizeId = BidderUtil.isNullOrZero(sizeId) - ? useVideoSizeLogic ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) : null + ? null : sizeId; validateVideoSizeId(resolvedSizeId, referer, imp.getId()); @@ -1030,23 +1042,6 @@ private static void validateVideoSizeId(Integer resolvedSizeId, String referer, } } - private static Integer resolveVideoSizeId(Integer placement, Integer instl) { - if (placement != null) { - if (placement == 1) { - return 201; - } - if (placement == 3) { - return 203; - } - } - - if (instl != null && instl == 1) { - return 202; - } - - return null; - } - private Banner makeBanner(Imp imp) { final Banner banner = imp.getBanner(); final Integer width = banner.getW(); @@ -1235,7 +1230,7 @@ private static Eid prepareExtUserEid(Eid extUserEid) { .filter(Objects::nonNull) .map(RubiconBidder::cleanExtUserEidUidStype) .toList(); - return Eid.of(extUserEid.getSource(), extUserEidUids, extUserEid.getExt()); + return extUserEid.toBuilder().uids(extUserEidUids).build(); } private static Uid cleanExtUserEidUidStype(Uid extUserEidUid) { @@ -1247,10 +1242,7 @@ private static Uid cleanExtUserEidUidStype(Uid extUserEidUid) { final ObjectNode extUserEidUidExtCopy = extUserEidUidExt.deepCopy(); extUserEidUidExtCopy.remove(STYPE_FIELD); - return Uid.of( - extUserEidUid.getId(), - extUserEidUid.getAtype(), - extUserEidUidExtCopy); + return extUserEidUid.toBuilder().ext(extUserEidUidExtCopy).build(); } private RubiconUserExtRp rubiconUserExtRp(User user, ExtImpRubicon rubiconImpExt) { @@ -1625,19 +1617,27 @@ private List bidsFromResponse(BidRequest prebidRequest, } private RubiconSeatBid updateSeatBids(RubiconSeatBid seatBid, List errors) { - final String buyer = seatBid.getBuyer(); - final int networkId = NumberUtils.toInt(buyer, 0); - if (networkId <= 0) { + final Integer networkId = resolveNetworkId(seatBid); + final String seat = seatBid.getSeat(); + + if (networkId == null && seat == null) { return seatBid; } + final List updatedBids = seatBid.getBid().stream() - .map(bid -> insertNetworkIdToMeta(bid, networkId, errors)) + .map(bid -> prepareBidMeta(bid, seat, networkId, errors)) .filter(Objects::nonNull) .toList(); return seatBid.toBuilder().bid(updatedBids).build(); } - private RubiconBid insertNetworkIdToMeta(RubiconBid bid, int networkId, List errors) { + private static Integer resolveNetworkId(RubiconSeatBid seatBid) { + final String buyer = seatBid.getBuyer(); + final int networkId = NumberUtils.toInt(buyer, 0); + return networkId <= 0 ? null : networkId; + } + + private RubiconBid prepareBidMeta(RubiconBid bid, String seat, Integer networkId, List errors) { final ObjectNode bidExt = bid.getExt(); final ExtPrebid extPrebid; try { @@ -1648,9 +1648,13 @@ private RubiconBid insertNetworkIdToMeta(RubiconBid bid, int networkId, List { new TypeReference<>() { }; + private static final String BIDDER_CURRENCY = "USD"; + + private final CurrencyConversionService currencyConversionService; private final String endpointUrl; private final JacksonMapper mapper; - public SonobiBidder(String endpointUrl, JacksonMapper mapper) { + public SonobiBidder(CurrencyConversionService currencyConversionService, + String endpointUrl, + JacksonMapper mapper) { + + this.currencyConversionService = currencyConversionService; this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -50,7 +60,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { final ExtImpSonobi extImpSonobi = parseImpExt(imp); - final Imp modifiedImp = modifyImp(imp, extImpSonobi.getTagId()); + final Imp modifiedImp = modifyImp(bidRequest, imp, extImpSonobi.getTagId()); requests.add(makeRequest(bidRequest, modifiedImp)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -68,34 +78,51 @@ private ExtImpSonobi parseImpExt(Imp imp) throws PreBidException { } } - private static Imp modifyImp(Imp imp, String tagId) { - return imp.toBuilder().tagid(tagId).build(); + private Imp modifyImp(BidRequest bidRequest, Imp imp, String tagId) { + final Price bidFloor = resolveBidFloor(bidRequest, imp); + return imp.toBuilder() + .tagid(tagId) + .bidfloor(bidFloor.getValue()) + .bidfloorcur(bidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + return Price.of( + BIDDER_CURRENCY, + currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY)); + } + + return Price.of(bidFloorCurrency, bidFloor); } private HttpRequest makeRequest(BidRequest bidRequest, Imp imp) { - final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(Collections.singletonList(imp)).build(); + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .cur(Collections.singletonList(BIDDER_CURRENCY)) + .imp(Collections.singletonList(imp)) + .build(); return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); } @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final BidResponse bidResponse; try { - bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - } catch (DecodeException e) { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } - - final List errors = new ArrayList<>(); - final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); - - return Result.of(bids, errors); } private static List extractBids(BidRequest bidRequest, - BidResponse bidResponse, - List errors) { + BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); @@ -107,26 +134,19 @@ private static List extractBids(BidRequest bidRequest, .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> makeBidderBid(bid, bidRequest.getImp(), bidResponse.getCur(), errors)) - .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), BIDDER_CURRENCY)) .toList(); } - private static BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { - try { - return BidderBid.of(bid, resolveBidType(bid.getImpid(), imps), currency); - } catch (PreBidException e) { - errors.add(BidderError.badServerResponse(e.getMessage())); - return null; - } - } - private static BidType resolveBidType(String impId, List imps) throws PreBidException { for (Imp imp : imps) { if (Objects.equals(impId, imp.getId())) { if (imp.getBanner() == null && imp.getVideo() != null) { return BidType.video; } + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() != null) { + return BidType.xNative; + } return BidType.banner; } } diff --git a/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java b/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java index c33ef88b520..cbd180f2e6f 100644 --- a/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java +++ b/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -151,13 +152,24 @@ private BidRequest createRequest(BidRequest request, List imps, ExtImpTaboo final List impExtBCat = impExt.getBCat(); final String impExtPageType = impExt.getPageType(); - final Site site = Optional.ofNullable(request.getSite()) - .map(Site::toBuilder) - .orElseGet(Site::builder) + final Publisher publisher = Publisher.builder().id(impExtPublisherId).build(); + + final Site site = request.getSite(); + final Site modifiedSite = site == null + ? null + : site.toBuilder() .id(impExtPublisherId) .name(impExtPublisherId) .domain(resolveDomain(impExt.getPublisherDomain(), request)) - .publisher(Publisher.builder().id(impExtPublisherId).build()) + .publisher(publisher) + .build(); + + final App app = request.getApp(); + final App modifiedApp = app == null + ? null + : app.toBuilder() + .id(impExtPublisherId) + .publisher(publisher) .build(); final ExtRequest extRequest = StringUtils.isNotEmpty(impExtPageType) @@ -166,7 +178,8 @@ private BidRequest createRequest(BidRequest request, List imps, ExtImpTaboo return request.toBuilder() .imp(imps) - .site(site) + .site(modifiedSite) + .app(modifiedApp) .badv(CollectionUtils.isNotEmpty(impExtBAdv) ? impExtBAdv : request.getBadv()) .bcat(CollectionUtils.isNotEmpty(impExtBCat) ? impExtBCat : request.getBcat()) .ext(extRequest) @@ -189,11 +202,11 @@ private ExtRequest createExtRequest(String pageType) { private HttpRequest createHttpRequest(MediaType type, BidRequest outgoingRequest) { return BidderUtil.defaultRequest(outgoingRequest, - buildEndpointUrl(outgoingRequest.getSite().getId(), type), + buildEndpointUrl(outgoingRequest, type), mapper); } - private String buildEndpointUrl(String publisherId, MediaType mediaType) { + private String buildEndpointUrl(BidRequest bidRequest, MediaType mediaType) { final String type = switch (mediaType) { case BANNER -> DISPLAY_ENDPOINT_PREFIX; case NATIVE -> NATIVE_ENDPOINT_PREFIX; @@ -202,6 +215,10 @@ private String buildEndpointUrl(String publisherId, MediaType mediaType) { default -> throw new AssertionError(); }; + final String publisherId = Optional.ofNullable(bidRequest.getSite()).map(Site::getId) + .or(() -> Optional.ofNullable(bidRequest.getApp()).map(App::getId)) + .orElse(StringUtils.EMPTY); + return endpointTemplate .replace("{{GvlID}}", gvlId) .replace("{{MediaType}}", type) diff --git a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java new file mode 100644 index 00000000000..c9a8366f9ac --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -0,0 +1,204 @@ +package org.prebid.server.bidder.thetradedesk; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +public class TheTradeDeskBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PREBID_INTEGRATION_TYPE_HEADER = "x-integration-type"; + private static final String PREBID_INTEGRATION_TYPE = "1"; + private static final MultiMap HEADERS = HttpUtil.headers() + .add(PREBID_INTEGRATION_TYPE_HEADER, PREBID_INTEGRATION_TYPE); + + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$"); + + private final String endpointUrl; + private final String supplyId; + private final JacksonMapper mapper; + + public TheTradeDeskBidder(String endpointUrl, JacksonMapper mapper, String supplyId) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.supplyId = validateSupplyId(supplyId); + this.mapper = Objects.requireNonNull(mapper); + } + + private static String validateSupplyId(String supplyId) { + if (StringUtils.isBlank(supplyId) || SUPPLY_ID_PATTERN.matcher(supplyId).matches()) { + return supplyId; + } + + throw new IllegalArgumentException("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + + String publisherId = null; + for (Imp imp : request.getImp()) { + try { + final ExtImpTheTradeDesk extImp = parseImpExt(imp); + publisherId = publisherId == null + ? StringUtils.isNotBlank(extImp.getPublisherId()) + ? extImp.getPublisherId() + : publisherId + : publisherId; + + modifiedImps.add(modifyImp(imp)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + HEADERS, + resolveEndpoint(), + mapper); + + return Result.withValue(httpRequest); + } + + private ExtImpTheTradeDesk parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); + } + } + + private static Imp modifyImp(Imp imp) { + final Banner banner = imp.getBanner(); + + if (banner != null && CollectionUtils.isNotEmpty(banner.getFormat())) { + final Format format = banner.getFormat().getFirst(); + return imp.toBuilder() + .banner(banner.toBuilder().w(format.getW()).h(format.getH()).build()) + .build(); + } + + return imp; + } + + private static BidRequest modifyRequest(BidRequest request, List modifiedImps, String publisherId) { + return request.toBuilder() + .imp(modifiedImps) + .site(modifySite(request, publisherId)) + .app(modifyApp(request, publisherId)) + .build(); + } + + private static Site modifySite(BidRequest request, String publisherId) { + final Site site = request.getSite(); + if (site == null) { + return null; + } + + return site.toBuilder() + .publisher(modifyPublisher(site.getPublisher(), publisherId)) + .build(); + } + + private static Publisher modifyPublisher(Publisher publisher, String publisherId) { + if (publisher == null) { + return Publisher.builder().id(publisherId).build(); + } + + return publisher.toBuilder() + .id(StringUtils.isNotBlank(publisherId) ? publisherId : publisher.getId()) + .build(); + } + + private static App modifyApp(BidRequest request, String publisherId) { + final Site site = request.getSite(); + final App app = request.getApp(); + + if (site != null) { + return app; + } + + if (app == null) { + return null; + } + + return app.toBuilder() + .publisher(modifyPublisher(app.getPublisher(), publisherId)) + .build(); + } + + private String resolveEndpoint() { + return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(supplyId))); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java b/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java new file mode 100644 index 00000000000..99d8bc8e74c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java @@ -0,0 +1,135 @@ +package org.prebid.server.bidder.tradplus; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.tradplus.ExtImpTradPlus; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class TradPlusBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + public static final String X_OPENRTB_VERSION = "2.5"; + + private static final String ZONE_ID = "{{ZoneID}}"; + private static final String ACCOUNT_ID = "{{AccountID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public TradPlusBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpTradPlus extImpTradPlus = parseImpExt(bidRequest.getImp().getFirst().getExt()); + validateImpExt(extImpTradPlus); + final HttpRequest httpRequest; + httpRequest = makeHttpRequest(extImpTradPlus, bidRequest.getImp(), bidRequest); + return Result.withValue(httpRequest); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpTradPlus parseImpExt(ObjectNode extNode) { + final ExtImpTradPlus extImpTradPlus; + try { + extImpTradPlus = mapper.mapper().convertValue(extNode, EXT_TYPE_REFERENCE).getBidder(); + return extImpTradPlus; + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private void validateImpExt(ExtImpTradPlus extImpTradPlus) { + if (StringUtils.isBlank(extImpTradPlus.getAccountId())) { + throw new PreBidException("Invalid/Missing AccountID"); + } + } + + private HttpRequest makeHttpRequest(ExtImpTradPlus extImpTradPlus, List imps, + BidRequest bidRequest) { + final String uri; + uri = endpointUrl.replace(ZONE_ID, extImpTradPlus.getZoneId()).replace(ACCOUNT_ID, + extImpTradPlus.getAccountId()); + + final BidRequest outgoingRequest = bidRequest.toBuilder().imp(removeImpsExt(imps)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, makeHeaders(), uri, mapper); + } + + private MultiMap makeHeaders() { + return HttpUtil.headers().set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + } + + private static List removeImpsExt(List imps) { + return imps.stream().map(imp -> imp.toBuilder().ext(null).build()).toList(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse, httpCall.getRequest().getPayload())); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, BidRequest bidRequest) { + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections + .emptyList() : bidsFromResponse(bidResponse, bidRequest.getImp()); + } + + private static List bidsFromResponse(BidResponse bidResponse, List imps) { + return bidResponse.getSeatbid().stream().filter(Objects::nonNull).map(SeatBid::getBid) + .filter(Objects::nonNull).flatMap(Collection::stream).map(bid -> BidderBid + .of(bid, getBidType(bid.getImpid(), imps), bidResponse.getCur())).toList(); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return BidType.banner; + } + } + throw new PreBidException( + "Invalid bid imp ID #%s does not match any imp IDs from the original bid request".formatted(impId)); + } + +} diff --git a/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java similarity index 86% rename from src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java rename to src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java index bba94b55f9c..f2c87bc2583 100644 --- a/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java +++ b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.liftoff; +package org.prebid.server.bidder.vungle; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; @@ -13,18 +13,18 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.liftoff.model.LiftoffImpressionExt; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.vungle.model.VungleImpressionExt; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -37,7 +37,7 @@ import java.util.List; import java.util.Objects; -public class LiftoffBidder implements Bidder { +public class VungleBidder implements Bidder { private static final String BIDDER_CURRENCY = "USD"; private static final String X_OPENRTB_VERSION = "2.5"; @@ -46,9 +46,9 @@ public class LiftoffBidder implements Bidder { private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public LiftoffBidder(String endpointUrl, - CurrencyConversionService currencyConversionService, - JacksonMapper mapper) { + public VungleBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.currencyConversionService = Objects.requireNonNull(currencyConversionService); @@ -63,8 +63,8 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { final Price price = resolveBidFloor(imp, bidRequest); - final LiftoffImpressionExt impExt = parseImpExt(imp); - final LiftoffImpressionExt modifiedImpExt = modifyImpExt(impExt, bidRequest); + final VungleImpressionExt impExt = parseImpExt(imp); + final VungleImpressionExt modifiedImpExt = modifyImpExt(impExt, bidRequest); final Imp modifiedImp = modifyImp(imp, modifiedImpExt, price); final BidRequest modifiedRequest = modifyBidRequest( bidRequest, @@ -92,14 +92,14 @@ private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { return Price.of(BIDDER_CURRENCY, bigDecimal); } - private LiftoffImpressionExt parseImpExt(Imp imp) { - return mapper.mapper().convertValue(imp.getExt(), LiftoffImpressionExt.class); + private VungleImpressionExt parseImpExt(Imp imp) { + return mapper.mapper().convertValue(imp.getExt(), VungleImpressionExt.class); } - private static LiftoffImpressionExt modifyImpExt(LiftoffImpressionExt impExt, BidRequest bidRequest) { - final ExtImpLiftoff bidder = impExt.getBidder(); + private static VungleImpressionExt modifyImpExt(VungleImpressionExt impExt, BidRequest bidRequest) { + final ExtImpVungle bidder = impExt.getBidder(); final String buyerId = ObjectUtil.getIfNotNull(bidRequest.getUser(), User::getBuyeruid); - final ExtImpLiftoff vungle = ExtImpLiftoff.of( + final ExtImpVungle vungle = ExtImpVungle.of( buyerId, bidder.getAppStoreId(), bidder.getPlacementReferenceId()); @@ -107,7 +107,7 @@ private static LiftoffImpressionExt modifyImpExt(LiftoffImpressionExt impExt, Bi return impExt.toBuilder().vungle(vungle).build(); } - private Imp modifyImp(Imp imp, LiftoffImpressionExt modifiedImpExt, Price price) { + private Imp modifyImp(Imp imp, VungleImpressionExt modifiedImpExt, Price price) { return imp.toBuilder() .tagid(modifiedImpExt.getBidder().getPlacementReferenceId()) .ext(mapper.mapper().convertValue(modifiedImpExt, ObjectNode.class)) diff --git a/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java b/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java new file mode 100644 index 00000000000..267d7368768 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.vungle.model; + +import lombok.Builder; +import lombok.Getter; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; + +@Builder(toBuilder = true) +@Getter +public class VungleImpressionExt { + + ExtImpPrebid prebid; + + ExtImpVungle bidder; + + ExtImpVungle vungle; +} diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index af1978d5206..cf99e41de84 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -49,6 +49,7 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -56,6 +57,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; public class YieldlabBidder implements Bidder { @@ -166,6 +168,12 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { .addParameter("ts", timestamp) .addParameter("t", getTargetingValues(extImpYieldlab)); + final String formats = makeFormats(request, extImpYieldlab); + + if (formats != null) { + uriBuilder.addParameter("sizes", formats); + } + final User user = request.getUser(); if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { uriBuilder.addParameter("ids", String.join("ylid:", user.getBuyeruid())); @@ -209,6 +217,24 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { return uriBuilder.toString(); } + private String makeFormats(BidRequest request, ExtImpYieldlab extImp) { + final List formats = new ArrayList<>(); + for (Imp imp: request.getImp()) { + if (isBanner(imp)) { + Stream.ofNullable(imp.getBanner().getFormat()) + .flatMap(Collection::stream) + .map(format -> "%s:%d|%d".formatted(extImp.getAdslotId(), format.getW(), format.getH())) + .forEach(formats::add); + } + } + + return formats.isEmpty() ? null : String.join(",", formats); + } + + private boolean isBanner(Imp imp) { + return imp.getBanner() != null && imp.getXNative() == null && imp.getVideo() == null && imp.getAudio() == null; + } + /** * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. */ diff --git a/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java index 6db93819349..956330cd1ca 100644 --- a/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java +++ b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java @@ -47,17 +47,17 @@ public Future storeEntry(String key, StorageDataType type, Integer ttlseconds, String application, - String moduleCode) { + String appCode) { try { - validateStoreData(key, value, application, type, moduleCode); + validateStoreData(key, value, application, type, appCode); } catch (PreBidException e) { return Future.failedFuture(e); } final ModuleCacheRequest moduleCacheRequest = ModuleCacheRequest.of( - constructEntryKey(key, moduleCode), + constructEntryKey(key, appCode), type, prepareValueForStoring(value, type), application, @@ -124,18 +124,18 @@ private Future processStoreResponse(int statusCode, String responseBody) { } @Override - public Future retrieveModuleEntry(String key, - String moduleCode, - String application) { + public Future retrieveEntry(String key, + String appCode, + String application) { try { - validateRetrieveData(key, application, moduleCode); + validateRetrieveData(key, application, appCode); } catch (PreBidException e) { return Future.failedFuture(e); } return httpClient.get( - getRetrieveEndpoint(key, moduleCode, application), + getRetrieveEndpoint(key, appCode, application), securedCallHeaders(), callTimeoutMs) .map(response -> toModuleCacheResponse(response.getStatusCode(), response.getBody())); diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index bcf839cd383..e86ac4f5d9b 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.response.Bid; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; @@ -26,7 +27,7 @@ import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.identity.UUIDIdGenerator; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -61,8 +62,6 @@ public class CoreCacheService { private static final Logger logger = LoggerFactory.getLogger(CoreCacheService.class); - private static final Map> DEBUG_HEADERS = - HttpUtil.toDebugHeaders(CacheServiceUtil.CACHE_HEADERS); private static final String BID_WURL_ATTRIBUTE = "wurl"; private final HttpClient httpClient; @@ -76,11 +75,16 @@ public class CoreCacheService { private final UUIDIdGenerator idGenerator; private final JacksonMapper mapper; + private final MultiMap cacheHeaders; + private final Map> debugHeaders; + public CoreCacheService( HttpClient httpClient, URL endpointUrl, String cachedAssetUrlTemplate, long expectedCacheTimeMs, + String apiKey, + boolean isApiKeySecured, VastModifier vastModifier, EventsService eventsService, Metrics metrics, @@ -98,6 +102,11 @@ public CoreCacheService( this.clock = Objects.requireNonNull(clock); this.idGenerator = Objects.requireNonNull(idGenerator); this.mapper = Objects.requireNonNull(mapper); + + cacheHeaders = isApiKeySecured + ? HttpUtil.headers().add(HttpUtil.X_PBC_API_KEY_HEADER, Objects.requireNonNull(apiKey)) + : HttpUtil.headers(); + debugHeaders = HttpUtil.toDebugHeaders(cacheHeaders); } public String getEndpointHost() { @@ -121,7 +130,10 @@ public String cacheVideoDebugLog(CachedDebugLog cachedDebugLog, Integer videoCac final List cachedCreatives = Collections.singletonList( makeDebugCacheCreative(cachedDebugLog, cacheKey, videoCacheTtl)); final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); - httpClient.post(endpointUrl.toString(), HttpUtil.headers(), mapper.encodeToString(bidCacheRequest), + httpClient.post( + endpointUrl.toString(), + cacheHeaders, + mapper.encodeToString(bidCacheRequest), expectedCacheTimeMs); return cacheKey; } @@ -155,7 +167,7 @@ private Future makeRequest(BidCacheRequest bidCacheRequest, final long startTime = clock.millis(); return httpClient.post( endpointUrl.toString(), - CacheServiceUtil.CACHE_HEADERS, + cacheHeaders, mapper.encodeToString(bidCacheRequest), remainingTimeout) .map(response -> toBidCacheResponse( @@ -238,7 +250,7 @@ private List getCacheBids(List bidInfos) { private List getVideoCacheBids(List bidInfos) { return bidInfos.stream() .filter(bidInfo -> Objects.equals(bidInfo.getBidType(), BidType.video)) - .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVideoTtl())) + .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVastTtl())) .toList(); } @@ -286,7 +298,7 @@ private Future doCacheOpenrtb(List bids, final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body); final long startTime = clock.millis(); - return httpClient.post(url, CacheServiceUtil.CACHE_HEADERS, body, remainingTimeout) + return httpClient.post(url, cacheHeaders, body, remainingTimeout) .map(response -> processResponseOpenrtb(response, httpRequest, cachedCreatives.size(), @@ -348,7 +360,7 @@ private DebugHttpCall makeDebugHttpCall(String endpoint, .responseStatus(httpResponse != null ? httpResponse.getStatusCode() : null) .responseBody(httpResponse != null ? httpResponse.getBody() : null) .responseTimeMillis(responseTime(startTime)) - .requestHeaders(DEBUG_HEADERS) + .requestHeaders(debugHeaders) .build(); } diff --git a/src/main/java/org/prebid/server/cache/PbcStorageService.java b/src/main/java/org/prebid/server/cache/PbcStorageService.java index 4e8d432a158..c76f0884d52 100644 --- a/src/main/java/org/prebid/server/cache/PbcStorageService.java +++ b/src/main/java/org/prebid/server/cache/PbcStorageService.java @@ -11,9 +11,9 @@ Future storeEntry(String key, StorageDataType type, Integer ttlseconds, String application, - String moduleCode); + String appCode); - Future retrieveModuleEntry(String key, String moduleCode, String application); + Future retrieveEntry(String key, String appCode, String application); static NoOpPbcStorageService noOp() { return new NoOpPbcStorageService(); @@ -27,13 +27,13 @@ public Future storeEntry(String key, StorageDataType type, Integer ttlseconds, String application, - String moduleCode) { + String appCode) { return Future.succeededFuture(); } @Override - public Future retrieveModuleEntry(String key, String moduleCode, String application) { + public Future retrieveEntry(String key, String appCode, String application) { return Future.succeededFuture(ModuleCacheResponse.empty()); } } diff --git a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java index 886c245122c..281313d25f5 100644 --- a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java +++ b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.bidder.UsersyncMethodChooser; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java b/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java deleted file mode 100644 index 8621e00dbce..00000000000 --- a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.execution; - -import io.vertx.core.Future; - -/** - * Contract fro services which use external files. - */ -public interface RemoteFileProcessor { - - Future setDataPath(String dataFilePath); -} - diff --git a/src/main/java/org/prebid/server/execution/file/FileProcessor.java b/src/main/java/org/prebid/server/execution/file/FileProcessor.java new file mode 100644 index 00000000000..f17ab4758ee --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/FileProcessor.java @@ -0,0 +1,8 @@ +package org.prebid.server.execution.file; + +import io.vertx.core.Future; + +public interface FileProcessor { + + Future setDataPath(String dataFilePath); +} diff --git a/src/main/java/org/prebid/server/execution/file/FileUtil.java b/src/main/java/org/prebid/server/execution/file/FileUtil.java new file mode 100644 index 00000000000..3de28c1992f --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/FileUtil.java @@ -0,0 +1,106 @@ +package org.prebid.server.execution.file; + +import io.vertx.core.Vertx; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.file.FileSystemException; +import io.vertx.core.http.HttpClientOptions; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.syncer.FileSyncer; +import org.prebid.server.execution.file.syncer.LocalFileSyncer; +import org.prebid.server.execution.file.syncer.RemoteFileSyncerV2; +import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; +import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; +import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.spring.config.model.ExponentialBackoffProperties; +import org.prebid.server.spring.config.model.FileSyncerProperties; +import org.prebid.server.spring.config.model.HttpClientProperties; + +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileUtil { + + private FileUtil() { + } + + public static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { + try { + final Path dirPath = Paths.get(filePath).getParent(); + final String dirPathString = dirPath.toString(); + final FileProps props = fileSystem.existsBlocking(dirPathString) + ? fileSystem.propsBlocking(dirPathString) + : null; + + if (props == null || !props.isDirectory()) { + fileSystem.mkdirsBlocking(dirPathString); + } else if (!Files.isWritable(dirPath)) { + throw new PreBidException("No write permissions for directory: " + dirPath); + } + } catch (FileSystemException | InvalidPathException e) { + throw new PreBidException("Cannot create directory for file: " + filePath, e); + } + } + + public static FileSyncer fileSyncerFor(FileProcessor fileProcessor, + FileSyncerProperties properties, + Vertx vertx) { + + return switch (properties.getType()) { + case LOCAL -> new LocalFileSyncer( + fileProcessor, + properties.getSaveFilepath(), + properties.getUpdateIntervalMs(), + toRetryPolicy(properties), + vertx); + case REMOTE -> remoteFileSyncer(fileProcessor, properties, vertx); + }; + } + + private static RemoteFileSyncerV2 remoteFileSyncer(FileProcessor fileProcessor, + FileSyncerProperties properties, + Vertx vertx) { + + final HttpClientProperties httpClientProperties = properties.getHttpClient(); + final HttpClientOptions httpClientOptions = new HttpClientOptions() + .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) + .setMaxRedirects(httpClientProperties.getMaxRedirects()); + + return new RemoteFileSyncerV2( + fileProcessor, + properties.getDownloadUrl(), + properties.getSaveFilepath(), + properties.getTmpFilepath(), + vertx.createHttpClient(httpClientOptions), + properties.getTimeoutMs(), + properties.isCheckSize(), + properties.getUpdateIntervalMs(), + toRetryPolicy(properties), + vertx); + } + + // TODO: remove after transition period + private static RetryPolicy toRetryPolicy(FileSyncerProperties properties) { + final Long retryIntervalMs = properties.getRetryIntervalMs(); + final Integer retryCount = properties.getRetryCount(); + final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount); + final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount) + || !fixedRetryPolicyDefined; + + if (!fixedRetryPolicyValid) { + throw new IllegalArgumentException("fixed interval retry policy is invalid"); + } + + final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry(); + return fixedRetryPolicyDefined + ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount) + : ExponentialBackoffRetryPolicy.of( + exponentialBackoffProperties.getDelayMillis(), + exponentialBackoffProperties.getMaxDelayMillis(), + exponentialBackoffProperties.getFactor(), + exponentialBackoffProperties.getJitter()); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java new file mode 100644 index 00000000000..55517caa9a7 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java @@ -0,0 +1,47 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +public class LocalFileSupplier implements Supplier> { + + private final String filePath; + private final FileSystem fileSystem; + private final AtomicLong lastSupplyTime; + + public LocalFileSupplier(String filePath, FileSystem fileSystem) { + this.filePath = Objects.requireNonNull(filePath); + this.fileSystem = Objects.requireNonNull(fileSystem); + lastSupplyTime = new AtomicLong(Long.MIN_VALUE); + } + + @Override + public Future get() { + return fileSystem.exists(filePath) + .compose(exists -> exists + ? fileSystem.props(filePath) + : Future.failedFuture("File %s not found.".formatted(filePath))) + .map(this::getFileIfModified); + } + + private String getFileIfModified(FileProps fileProps) { + final long lastModifiedTime = lasModifiedTime(fileProps); + final long lastSupplyTime = this.lastSupplyTime.get(); + + if (lastSupplyTime < lastModifiedTime) { + this.lastSupplyTime.compareAndSet(lastSupplyTime, lastModifiedTime); + return filePath; + } + + return null; + } + + private static long lasModifiedTime(FileProps fileProps) { + return Math.max(fileProps.creationTime(), fileProps.lastModifiedTime()); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java new file mode 100644 index 00000000000..e8b8f313c54 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java @@ -0,0 +1,160 @@ +package org.prebid.server.execution.file.supplier; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.file.CopyOptions; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.file.OpenOptions; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.util.HttpUtil; + +import java.util.Objects; +import java.util.function.Supplier; + +public class RemoteFileSupplier implements Supplier> { + + private static final Logger logger = LoggerFactory.getLogger(RemoteFileSupplier.class); + + private final String savePath; + private final String backupPath; + private final String tmpPath; + private final HttpClient httpClient; + private final FileSystem fileSystem; + + private final RequestOptions getRequestOptions; + private final RequestOptions headRequestOptions; + + public RemoteFileSupplier(String downloadUrl, + String savePath, + String tmpPath, + HttpClient httpClient, + long timeout, + boolean checkRemoteFileSize, + FileSystem fileSystem) { + + this.savePath = Objects.requireNonNull(savePath); + this.backupPath = savePath + ".old"; + this.tmpPath = Objects.requireNonNull(tmpPath); + this.httpClient = Objects.requireNonNull(httpClient); + this.fileSystem = Objects.requireNonNull(fileSystem); + + HttpUtil.validateUrl(downloadUrl); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, savePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, backupPath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpPath); + + getRequestOptions = new RequestOptions() + .setMethod(HttpMethod.GET) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); + headRequestOptions = checkRemoteFileSize + ? new RequestOptions() + .setMethod(HttpMethod.HEAD) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true) + : null; + } + + @Override + public Future get() { + return isDownloadRequired().compose(isDownloadRequired -> isDownloadRequired + ? Future.all(downloadFile(), createBackup()) + .compose(ignored -> tmpToSave()) + .map(savePath) + : Future.succeededFuture()); + } + + private Future isDownloadRequired() { + return headRequestOptions != null + ? fileSystem.exists(savePath) + .compose(exists -> exists ? isSizeChanged() : Future.succeededFuture(true)) + : Future.succeededFuture(true); + } + + private Future isSizeChanged() { + final Future localFileSize = fileSystem.props(savePath).map(FileProps::size); + final Future remoteFileSize = sendHttpRequest(headRequestOptions) + .map(response -> response.getHeader(HttpHeaders.CONTENT_LENGTH)) + .map(Long::parseLong); + + return Future.all(localFileSize, remoteFileSize) + .map(compositeResult -> !Objects.equals(compositeResult.resultAt(0), compositeResult.resultAt(1))); + } + + private Future downloadFile() { + return fileSystem.open(tmpPath, new OpenOptions()) + .compose(tmpFile -> sendHttpRequest(getRequestOptions) + .compose(response -> response.pipeTo(tmpFile)) + .onComplete(result -> tmpFile.close())); + } + + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .map(this::validateResponse); + } + + private HttpClientResponse validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + throw new PreBidException("Got unexpected response from server with status code %s and message %s" + .formatted(statusCode, response.statusMessage())); + } + + return response; + } + + private Future tmpToSave() { + return copyFile(tmpPath, savePath); + } + + public void clearTmp() { + fileSystem.exists(tmpPath).onSuccess(exists -> { + if (exists) { + deleteFile(tmpPath); + } + }); + } + + private Future createBackup() { + return fileSystem.exists(savePath) + .compose(exists -> exists ? copyFile(savePath, backupPath) : Future.succeededFuture()); + } + + public void deleteBackup() { + fileSystem.exists(backupPath).onSuccess(exists -> { + if (exists) { + deleteFile(backupPath); + } + }); + } + + public Future restoreFromBackup() { + return fileSystem.exists(backupPath) + .compose(exists -> exists + ? copyFile(backupPath, savePath) + .onSuccess(ignored -> deleteFile(backupPath)) + : Future.succeededFuture()); + } + + private Future copyFile(String from, String to) { + return fileSystem.move(from, to, new CopyOptions().setReplaceExisting(true)); + } + + private void deleteFile(String filePath) { + fileSystem.delete(filePath) + .onFailure(error -> logger.error("Can't delete file: " + filePath)); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java new file mode 100644 index 00000000000..fd850e126c4 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java @@ -0,0 +1,84 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.retry.Retryable; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; +import java.util.function.Function; + +public abstract class FileSyncer { + + private static final Logger logger = LoggerFactory.getLogger(FileSyncer.class); + + private final FileProcessor fileProcessor; + private final long updatePeriod; + private final RetryPolicy retryPolicy; + private final Vertx vertx; + + protected FileSyncer(FileProcessor fileProcessor, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + this.fileProcessor = Objects.requireNonNull(fileProcessor); + this.updatePeriod = updatePeriod; + this.retryPolicy = Objects.requireNonNull(retryPolicy); + this.vertx = Objects.requireNonNull(vertx); + } + + public void sync() { + sync(retryPolicy); + } + + private void sync(RetryPolicy currentRetryPolicy) { + getFile() + .compose(this::processFile) + .onSuccess(ignored -> onSuccess()) + .onFailure(failure -> onFailure(currentRetryPolicy, failure)); + } + + protected abstract Future getFile(); + + private Future processFile(String filePath) { + return filePath != null + ? vertx.executeBlocking(() -> fileProcessor.setDataPath(filePath)) + .compose(Function.identity()) + .onFailure(error -> logger.error("Can't process saved file: " + filePath)) + : Future.succeededFuture(); + } + + private void onSuccess() { + doOnSuccess().onComplete(ignored -> setUpDeferredUpdate()); + } + + protected abstract Future doOnSuccess(); + + private void setUpDeferredUpdate() { + if (updatePeriod > 0) { + vertx.setTimer(updatePeriod, ignored -> sync()); + } + } + + private void onFailure(RetryPolicy currentRetryPolicy, Throwable failure) { + doOnFailure(failure).onComplete(ignored -> retrySync(currentRetryPolicy)); + } + + protected abstract Future doOnFailure(Throwable throwable); + + private void retrySync(RetryPolicy currentRetryPolicy) { + if (currentRetryPolicy instanceof Retryable policy) { + logger.info( + "Retrying file sync for {} with policy: {}", + fileProcessor.getClass().getSimpleName(), + policy); + vertx.setTimer(policy.delay(), timerId -> sync(policy.next())); + } else { + setUpDeferredUpdate(); + } + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java new file mode 100644 index 00000000000..6ea109185b5 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java @@ -0,0 +1,38 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.supplier.LocalFileSupplier; +import org.prebid.server.execution.retry.RetryPolicy; + +public class LocalFileSyncer extends FileSyncer { + + private final LocalFileSupplier localFileSupplier; + + public LocalFileSyncer(FileProcessor fileProcessor, + String localFile, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + + localFileSupplier = new LocalFileSupplier(localFile, vertx.fileSystem()); + } + + @Override + protected Future getFile() { + return localFileSupplier.get(); + } + + @Override + protected Future doOnSuccess() { + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + return Future.succeededFuture(); + } +} diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java similarity index 77% rename from src/main/java/org/prebid/server/execution/RemoteFileSyncer.java rename to src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java index 9a6416ba44c..8deb838646f 100644 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java @@ -1,12 +1,11 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.file.syncer; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.file.CopyOptions; -import io.vertx.core.file.FileProps; import io.vertx.core.file.FileSystem; -import io.vertx.core.file.FileSystemException; import io.vertx.core.file.OpenOptions; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; @@ -16,22 +15,23 @@ import io.vertx.core.http.RequestOptions; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.FileUtil; import org.prebid.server.execution.retry.RetryPolicy; import org.prebid.server.execution.retry.Retryable; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Paths; import java.util.Objects; +import java.util.function.Function; +@Deprecated public class RemoteFileSyncer { private static final Logger logger = LoggerFactory.getLogger(RemoteFileSyncer.class); - private final RemoteFileProcessor processor; + private final FileProcessor processor; private final String downloadUrl; private final String saveFilePath; private final String tmpFilePath; @@ -43,7 +43,7 @@ public class RemoteFileSyncer { private final RequestOptions getFileRequestOptions; private final RequestOptions isUpdateRequiredRequestOptions; - public RemoteFileSyncer(RemoteFileProcessor processor, + public RemoteFileSyncer(FileProcessor processor, String downloadUrl, String saveFilePath, String tmpFilePath, @@ -63,32 +63,20 @@ public RemoteFileSyncer(RemoteFileProcessor processor, this.vertx = Objects.requireNonNull(vertx); this.fileSystem = vertx.fileSystem(); - createAndCheckWritePermissionsFor(fileSystem, saveFilePath); - createAndCheckWritePermissionsFor(fileSystem, tmpFilePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, saveFilePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpFilePath); getFileRequestOptions = new RequestOptions() .setMethod(HttpMethod.GET) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); isUpdateRequiredRequestOptions = new RequestOptions() .setMethod(HttpMethod.HEAD) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); - } - - private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { - try { - final String dirPath = Paths.get(filePath).getParent().toString(); - final FileProps props = fileSystem.existsBlocking(dirPath) ? fileSystem.propsBlocking(dirPath) : null; - if (props == null || !props.isDirectory()) { - fileSystem.mkdirsBlocking(dirPath); - } else if (!Files.isWritable(Paths.get(dirPath))) { - throw new PreBidException("No write permissions for directory: " + dirPath); - } - } catch (FileSystemException | InvalidPathException e) { - throw new PreBidException("Cannot create directory for file: " + filePath, e); - } + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); } public void sync() { @@ -98,7 +86,8 @@ public void sync() { } private Future processSavedFile() { - return processor.setDataPath(saveFilePath) + return vertx.executeBlocking(() -> processor.setDataPath(saveFilePath)) + .compose(Function.identity()) .onFailure(error -> logger.error("Can't process saved file: " + saveFilePath)) .recover(ignored -> deleteFile(saveFilePath).mapEmpty()) .mapEmpty(); @@ -112,8 +101,7 @@ private Future deleteFile(String filePath) { private Future syncRemoteFile(RetryPolicy retryPolicy) { return fileSystem.open(tmpFilePath, new OpenOptions()) - .compose(tmpFile -> httpClient.request(getFileRequestOptions) - .compose(HttpClientRequest::send) + .compose(tmpFile -> sendHttpRequest(getFileRequestOptions) .compose(response -> response.pipeTo(tmpFile)) .onComplete(result -> tmpFile.close())) @@ -148,8 +136,7 @@ private void setUpDeferredUpdate() { } private void updateIfNeeded() { - httpClient.request(isUpdateRequiredRequestOptions) - .compose(HttpClientRequest::send) + sendHttpRequest(isUpdateRequiredRequestOptions) .compose(response -> fileSystem.exists(saveFilePath) .compose(exists -> exists ? isLengthChanged(response) @@ -161,6 +148,24 @@ private void updateIfNeeded() { }); } + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .compose(this::validateResponse); + } + + private Future validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + return Future.failedFuture(new PreBidException( + String.format("Got unexpected response from server with status code %s and message %s", + statusCode, + response.statusMessage()))); + } else { + return Future.succeededFuture(response); + } + } + private Future isLengthChanged(HttpClientResponse response) { final String contentLengthParameter = response.getHeader(HttpHeaders.CONTENT_LENGTH); return StringUtils.isNumeric(contentLengthParameter) && !contentLengthParameter.equals("0") diff --git a/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java new file mode 100644 index 00000000000..54755dccc19 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java @@ -0,0 +1,69 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.file.FileSystem; +import io.vertx.core.http.HttpClient; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.supplier.LocalFileSupplier; +import org.prebid.server.execution.file.supplier.RemoteFileSupplier; +import org.prebid.server.execution.retry.RetryPolicy; + +public class RemoteFileSyncerV2 extends FileSyncer { + + private final LocalFileSupplier localFileSupplier; + private final RemoteFileSupplier remoteFileSupplier; + + public RemoteFileSyncerV2(FileProcessor fileProcessor, + String downloadUrl, + String saveFilePath, + String tmpFilePath, + HttpClient httpClient, + long timeout, + boolean checkSize, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + + final FileSystem fileSystem = vertx.fileSystem(); + localFileSupplier = new LocalFileSupplier(saveFilePath, fileSystem); + remoteFileSupplier = new RemoteFileSupplier( + downloadUrl, + saveFilePath, + tmpFilePath, + httpClient, + timeout, + checkSize, + fileSystem); + } + + @Override + protected Future getFile() { + return localFileSupplier.get() + .otherwiseEmpty() + .compose(localFile -> localFile != null + ? Future.succeededFuture(localFile) + : remoteFileSupplier.get()); + } + + @Override + protected Future doOnSuccess() { + remoteFileSupplier.clearTmp(); + remoteFileSupplier.deleteBackup(); + forceLastSupplyTimeUpdate(); + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + remoteFileSupplier.clearTmp(); + return remoteFileSupplier.restoreFromBackup() + .onSuccess(ignore -> forceLastSupplyTimeUpdate()); + } + + private void forceLastSupplyTimeUpdate() { + localFileSupplier.get(); + } +} diff --git a/src/main/java/org/prebid/server/execution/Timeout.java b/src/main/java/org/prebid/server/execution/timeout/Timeout.java similarity index 95% rename from src/main/java/org/prebid/server/execution/Timeout.java rename to src/main/java/org/prebid/server/execution/timeout/Timeout.java index f5abf239c87..b0f37e439fc 100644 --- a/src/main/java/org/prebid/server/execution/Timeout.java +++ b/src/main/java/org/prebid/server/execution/timeout/Timeout.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import lombok.Getter; diff --git a/src/main/java/org/prebid/server/execution/TimeoutFactory.java b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java similarity index 95% rename from src/main/java/org/prebid/server/execution/TimeoutFactory.java rename to src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java index cbe2768af1a..ae2624c8585 100644 --- a/src/main/java/org/prebid/server/execution/TimeoutFactory.java +++ b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import java.time.Clock; diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 04d0188400d..132bf86e782 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -179,7 +179,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, "Bid with id '%s' was rejected by floor enforcement: price %s is below the floor %s" .formatted(bid.getId(), price, floor), impId)); - rejectionTracker.reject(impId, BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); + rejectionTracker.rejectBid(bidderBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); updatedBidderBids.remove(bidderBid); } } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index c8163131c6e..e2983875386 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; public class BasicPriceFloorProcessor implements PriceFloorProcessor { @@ -45,6 +46,7 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; + private static final int USE_FETCH_DATA_RATE_MAX = 100; private static final int MODEL_WEIGHT_MAX_VALUE = 100; private static final int MODEL_WEIGHT_MIN_VALUE = 1; @@ -126,7 +128,7 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li final FetchResult fetchResult = floorFetcher.fetch(account); final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus); - if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) { + if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) { final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData()); return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch); } @@ -147,13 +149,20 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData); } - private static boolean shouldUseDynamicData(Account account) { - final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); - final AccountPriceFloorsConfig floorsConfig = - ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); + private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) { + final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceFloors) + .map(AccountPriceFloorsConfig::getUseDynamicData) + .map(BooleanUtils::isNotFalse) + .orElse(true); - return BooleanUtils.isNotFalse( - ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getUseDynamicData)); + final boolean shouldUseDynamicData = Optional.ofNullable(fetchResult.getRulesData()) + .map(PriceFloorData::getUseFetchDataRate) + .map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate) + .orElse(true); + + return isUsingDynamicDataAllowed && shouldUseDynamicData; } private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index 0692dce090a..0195c344474 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -13,7 +13,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.http.HttpStatus; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.proto.FetchResult; diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java index b976ea69c97..a5f97299340 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -17,6 +17,8 @@ public class PriceFloorRulesValidator { private static final int MODEL_WEIGHT_MIN_VALUE = 1; private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; + private static final int USE_FETCH_DATA_RATE_MIN = 0; + private static final int USE_FETCH_DATA_RATE_MAX = 100; private PriceFloorRulesValidator() { } @@ -48,6 +50,14 @@ public static void validateRulesData(PriceFloorData priceFloorData, Integer maxR "Price floor data skipRate must be in range(0-100), but was " + dataSkipRate); } + final Integer useFetchDataRate = priceFloorData.getUseFetchDataRate(); + if (useFetchDataRate != null + && (useFetchDataRate < USE_FETCH_DATA_RATE_MIN || useFetchDataRate > USE_FETCH_DATA_RATE_MAX)) { + + throw new PreBidException( + "Price floor data useFetchDataRate must be in range(0-100), but was " + useFetchDataRate); + } + if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) { throw new PreBidException("Price floor rules should contain at least one model group"); } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java index bdca465af36..4604ed91892 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java @@ -18,6 +18,9 @@ public class PriceFloorData { @JsonProperty("skipRate") Integer skipRate; + @JsonProperty("useFetchDataRate") + Integer useFetchDataRate; + @JsonProperty("floorsSchemaVersion") String floorsSchemaVersion; diff --git a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java index 791a1da06c1..268de61b246 100755 --- a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java index 72ec6feb2b2..30d78ea27c0 100644 --- a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java @@ -1,7 +1,7 @@ package org.prebid.server.geolocation; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.geolocation.model.GeoInfoConfiguration; diff --git a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java index 7604a25c71a..3d4c582db38 100644 --- a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java @@ -1,7 +1,7 @@ package org.prebid.server.geolocation; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; /** diff --git a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java index 2cea7119714..5afa9311cba 100644 --- a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java @@ -14,8 +14,8 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.execution.RemoteFileProcessor; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import java.io.FileInputStream; @@ -28,7 +28,7 @@ * Implementation of the {@link GeoLocationService} * backed by MaxMind free database */ -public class MaxMindGeoLocationService implements GeoLocationService, RemoteFileProcessor { +public class MaxMindGeoLocationService implements GeoLocationService, FileProcessor { private static final String VENDOR = "maxmind"; diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 746dffb5fc7..3ac0f44069c 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -24,8 +24,8 @@ import org.prebid.server.cookie.model.CookieSyncContext; import org.prebid.server.cookie.model.PartitionedCookie; import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; diff --git a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java index 971cb82204f..60e11195c26 100644 --- a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java +++ b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java @@ -18,7 +18,7 @@ import org.prebid.server.events.EventRequest; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.Endpoint; diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index c036bb310cd..4425e698458 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -37,8 +37,8 @@ import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/VtrackHandler.java index 6539881eaa4..3d1243264d9 100644 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/VtrackHandler.java @@ -17,8 +17,8 @@ import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.DecodeException; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java index c1d9c58dca6..a7b39dce659 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java @@ -22,7 +22,10 @@ import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.Tuple2; import org.prebid.server.auction.requestfactory.AmpRequestFactory; @@ -34,6 +37,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.HttpInteractionLogger; @@ -81,12 +86,14 @@ public class AmpHandler implements ApplicationResource { private final ExchangeService exchangeService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; private final Clock clock; private final BidderCatalog bidderCatalog; private final Set biddersSupportingCustomTargeting; private final AmpResponsePostProcessor ampResponsePostProcessor; private final HttpInteractionLogger httpInteractionLogger; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; private final double logSamplingRate; @@ -94,12 +101,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, ExchangeService exchangeService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, BidderCatalog bidderCatalog, Set biddersSupportingCustomTargeting, AmpResponsePostProcessor ampResponsePostProcessor, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper, double logSamplingRate) { @@ -107,12 +116,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, this.exchangeService = Objects.requireNonNull(exchangeService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.biddersSupportingCustomTargeting = Objects.requireNonNull(biddersSupportingCustomTargeting); this.ampResponsePostProcessor = Objects.requireNonNull(ampResponsePostProcessor); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; } @@ -134,18 +145,25 @@ public void handle(RoutingContext routingContext) { .httpContext(HttpRequestContext.from(routingContext)); ampRequestFactory.fromRequest(routingContext, startTime) - .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) .map(this::updateAppAndNoCookieAndImpsMetrics) - .compose(exchangeService::holdAuction) - .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), ampEventBuilder::bidResponse, context)) - .compose(context -> prepareAmpResponse(context, routingContext)) - .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result)) + .map(context -> addContextAndBidResponseToEvent(context, ampEventBuilder, context)) + .compose(context -> prepareSuccessfulResponse(context, routingContext, ampEventBuilder)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent(context.getAuctionContext(), ampEventBuilder, context)) .onComplete(responseResult -> handleResult(responseResult, ampEventBuilder, routingContext, startTime)); } + private static R addContextAndBidResponseToEvent(AuctionContext context, + AmpEvent.AmpEventBuilder ampEventBuilder, + R result) { + + ampEventBuilder.auctionContext(context); + ampEventBuilder.bidResponse(context.getBidResponse()); + return result; + } + private static R addToEvent(T field, Consumer consumer, R result) { consumer.accept(field); return result; @@ -166,8 +184,44 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context return context; } + private Future prepareSuccessfulResponse(AuctionContext auctionContext, + RoutingContext routingContext, + AmpEvent.AmpEventBuilder ampEventBuilder) { + + final String origin = originFrom(routingContext); + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext, origin) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return prepareAmpResponse(auctionContext, routingContext) + .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result)) + .map(result -> RawResponseContext.builder() + .responseBody(mapper.encodeToString(result.getLeft())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build()); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + private Future> prepareAmpResponse(AuctionContext context, RoutingContext routingContext) { + final BidRequest bidRequest = context.getBidRequest(); final BidResponse bidResponse = context.getBidResponse(); final AmpResponse ampResponse = toAmpResponse(bidResponse); @@ -271,12 +325,13 @@ private static ExtAmpVideoResponse extResponseFrom(BidResponse bidResponse) { : null; } - private void handleResult(AsyncResult> responseResult, + private void handleResult(AsyncResult responseResult, AmpEvent.AmpEventBuilder ampEventBuilder, RoutingContext routingContext, long startTime) { final boolean responseSucceeded = responseResult.succeeded(); + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; final MetricName metricRequestStatus; final List errorMessages; @@ -287,16 +342,22 @@ private void handleResult(AsyncResult> respo ampEventBuilder.origin(origin); final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext, origin); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(responseResult.result().getLeft()); + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext, origin) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException invalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -355,8 +416,7 @@ private void handleResult(AsyncResult> respo final int statusCode = status.code(); final AmpEvent ampEvent = ampEventBuilder.status(statusCode).errors(errorMessages).build(); - - final AuctionContext auctionContext = responseSucceeded ? responseResult.result().getRight() : null; + final AuctionContext auctionContext = ampEvent.getAuctionContext(); final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); @@ -406,8 +466,8 @@ private void handleResponseException(Throwable exception) { metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext, String origin) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext, String origin) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -419,10 +479,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext, Stri // Add AMP headers responseHeaders.add("AMP-Access-Control-Allow-Source-Origin", origin) .add("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"); - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - final MultiMap headers = response.headers(); - headers.add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java index b8664bc75fd..e0dbe2ea4e1 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java @@ -12,7 +12,10 @@ import io.vertx.ext.web.RoutingContext; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; @@ -22,6 +25,8 @@ import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.HttpInteractionLogger; @@ -55,9 +60,11 @@ public class AuctionHandler implements ApplicationResource { private final SkippedAuctionService skippedAuctionService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; private final Clock clock; private final HttpInteractionLogger httpInteractionLogger; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; public AuctionHandler(double logSamplingRate, @@ -66,9 +73,11 @@ public AuctionHandler(double logSamplingRate, SkippedAuctionService skippedAuctionService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { this.logSamplingRate = logSamplingRate; @@ -77,9 +86,11 @@ public AuctionHandler(double logSamplingRate, this.skippedAuctionService = Objects.requireNonNull(skippedAuctionService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); } @@ -102,7 +113,21 @@ public void handle(RoutingContext routingContext) { auctionRequestFactory.parseRequest(routingContext, startTime) .compose(auctionContext -> skippedAuctionService.skipAuction(auctionContext) .recover(throwable -> holdAuction(auctionEventBuilder, auctionContext))) - .onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime)); + .map(context -> addContextAndBidResponseToEvent(context, auctionEventBuilder, context)) + .map(context -> prepareSuccessfulResponse(context, routingContext)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent( + context.getAuctionContext(), auctionEventBuilder, context)) + .onComplete(result -> handleResult(result, auctionEventBuilder, routingContext, startTime)); + } + + private static R addContextAndBidResponseToEvent(AuctionContext context, + AuctionEvent.AuctionEventBuilder auctionEventBuilder, + R result) { + + auctionEventBuilder.auctionContext(context); + auctionEventBuilder.bidResponse(context.getBidResponse()); + return result; } private Future holdAuction(AuctionEvent.AuctionEventBuilder auctionEventBuilder, @@ -110,14 +135,9 @@ private Future holdAuction(AuctionEvent.AuctionEventBuilder auct return auctionRequestFactory.enrichAuctionContext(auctionContext) .map(this::updateAppAndNoCookieAndImpsMetrics) - // In case of holdAuction Exception and auctionContext is not present below .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - - .compose(exchangeService::holdAuction) - // populate event with updated context - .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context)); + .compose(exchangeService::holdAuction); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -142,14 +162,53 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context return context; } - private void handleResult(AsyncResult responseResult, + private RawResponseContext prepareSuccessfulResponse(AuctionContext auctionContext, RoutingContext routingContext) { + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return RawResponseContext.builder() + .responseBody(mapper.encodeToString(auctionContext.getBidResponse())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build(); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + + if (auctionContext.isAuctionSkipped()) { + return Future.succeededFuture(auctionContext) + .map(hooksMetricsService::updateHooksMetrics) + .map(rawResponseContext); + } + + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + + private void handleResult(AsyncResult responseResult, AuctionEvent.AuctionEventBuilder auctionEventBuilder, RoutingContext routingContext, long startTime) { final boolean responseSucceeded = responseResult.succeeded(); - final AuctionContext auctionContext = responseSucceeded ? responseResult.result() : null; + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; + final AuctionContext auctionContext = rawResponseContext != null + ? rawResponseContext.getAuctionContext() + : null; final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped(); final MetricName requestType = responseSucceeded ? auctionContext.getRequestTypeMetric() @@ -161,16 +220,22 @@ private void handleResult(AsyncResult responseResult, final String body; final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(responseResult.result().getBidResponse()); + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException invalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -263,8 +328,8 @@ private void handleResponseException(Throwable throwable, MetricName requestType metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -272,10 +337,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); } - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - response.headers() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java new file mode 100644 index 00000000000..5fe80a55c1d --- /dev/null +++ b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java @@ -0,0 +1,18 @@ +package org.prebid.server.handler.openrtb2; + +import io.vertx.core.MultiMap; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.auction.model.AuctionContext; + +@Value(staticConstructor = "of") +@Builder(toBuilder = true) +public class RawResponseContext { + + AuctionContext auctionContext; + + String responseBody; + + MultiMap responseHeaders; + +} diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java index d5957c15aa7..0bb31bab72b 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -1,15 +1,20 @@ package org.prebid.server.handler.openrtb2; +import com.iab.openrtb.request.video.PodError; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; +import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.CachedDebugLog; @@ -18,6 +23,8 @@ import org.prebid.server.cache.CoreCacheService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -56,17 +63,22 @@ public class VideoHandler implements ApplicationResource { private final CoreCacheService coreCacheService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; private final Clock clock; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; public VideoHandler(VideoRequestFactory videoRequestFactory, VideoResponseFactory videoResponseFactory, ExchangeService exchangeService, - CoreCacheService coreCacheService, AnalyticsReporterDelegator analyticsDelegator, + CoreCacheService coreCacheService, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { this.videoRequestFactory = Objects.requireNonNull(videoRequestFactory); @@ -75,8 +87,10 @@ public VideoHandler(VideoRequestFactory videoRequestFactory, this.coreCacheService = Objects.requireNonNull(coreCacheService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); } @@ -106,13 +120,55 @@ public void handle(RoutingContext routingContext) { .map(contextToErrors -> addToEvent(contextToErrors.getData(), videoEventBuilder::auctionContext, contextToErrors)) - .map(result -> videoResponseFactory.toVideoResponse( - result.getData(), result.getData().getBidResponse(), - result.getPodErrors())) + .compose(contextToErrors -> + prepareSuccessfulResponse(contextToErrors, routingContext, videoEventBuilder) + .compose(this::invokeExitpointHooks) + .compose(context -> toVideoResponse(context.getAuctionContext(), contextToErrors.getPodErrors()) + .map(videoResponse -> + addToEvent(videoResponse, videoEventBuilder::bidResponse, context))) + .map(context -> + addToEvent(context.getAuctionContext(), videoEventBuilder::auctionContext, context))) + .onComplete(result -> handleResult(result, videoEventBuilder, routingContext, startTime)); + } + + private Future prepareSuccessfulResponse(WithPodErrors context, + RoutingContext routingContext, + VideoEvent.VideoEventBuilder videoEventBuilder) { + + final AuctionContext auctionContext = context.getData(); + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return toVideoResponse(auctionContext, context.getPodErrors()) .map(videoResponse -> addToEvent(videoResponse, videoEventBuilder::bidResponse, videoResponse)) - .onComplete(responseResult -> handleResult(responseResult, videoEventBuilder, routingContext, - startTime)); + .map(videoResponse -> RawResponseContext.builder() + .responseBody(mapper.encodeToString(videoResponse)) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build()); + } + + private Future toVideoResponse(AuctionContext auctionContext, List podErrors) { + return Future.succeededFuture( + videoResponseFactory.toVideoResponse(auctionContext, auctionContext.getBidResponse(), podErrors)); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -120,7 +176,7 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private void handleResult(AsyncResult responseResult, + private void handleResult(AsyncResult responseResult, VideoEvent.VideoEventBuilder videoEventBuilder, RoutingContext routingContext, long startTime) { @@ -130,19 +186,25 @@ private void handleResult(AsyncResult responseResult, final List errorMessages; final HttpResponseStatus status; final String body; - final VideoResponse videoResponse = responseSucceeded ? responseResult.result() : null; + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(videoResponse); + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -240,8 +302,8 @@ private void handleResponseException(Throwable throwable) { metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -249,10 +311,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); } - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - response.headers() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java index 6243f9ed8c7..97bd43c4abf 100644 --- a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java +++ b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java @@ -1,7 +1,7 @@ package org.prebid.server.health; import io.vertx.core.Vertx; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.model.Status; import org.prebid.server.health.model.StatusResponse; diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java index 2525651f872..18d52b64c99 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -7,42 +7,41 @@ import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.provider.HookProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.LoggerFactory; import java.time.Clock; +import java.util.Map; import java.util.concurrent.TimeoutException; -import java.util.function.Function; import java.util.function.Supplier; class GroupExecutor { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(GroupExecutor.class)); - private final Vertx vertx; private final Clock clock; + private final Map modulesExecution; private ExecutionGroup group; private PAYLOAD initialPayload; - private Function> hookProvider; + private HookProvider hookProvider; private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; - private GroupExecutor(Vertx vertx, Clock clock) { + private GroupExecutor(Vertx vertx, Clock clock, Map modulesExecution) { this.vertx = vertx; this.clock = clock; + this.modulesExecution = modulesExecution; } public static GroupExecutor create( Vertx vertx, - Clock clock) { + Clock clock, + Map modulesExecution) { - return new GroupExecutor<>(vertx, clock); + return new GroupExecutor<>(vertx, clock, modulesExecution); } public GroupExecutor withGroup(ExecutionGroup group) { @@ -55,7 +54,7 @@ public GroupExecutor withInitialPayload(PAYLOAD initialPayload return this; } - public GroupExecutor withHookProvider(Function> hookProvider) { + public GroupExecutor withHookProvider(HookProvider hookProvider) { this.hookProvider = hookProvider; return this; } @@ -82,11 +81,15 @@ public Future> execute() { Future> groupFuture = Future.succeededFuture(initialGroupResult); for (final HookId hookId : group.getHookSequence()) { - final Hook hook = hookProvider.apply(hookId); + if (!modulesExecution.get(hookId.getModuleCode())) { + continue; + } + + final Future> hookFuture = hook(hookId); final long startTime = clock.millis(); - final Future> invocationResult = - executeHook(hook, group.getTimeout(), initialGroupResult, hookId); + final Future> invocationResult = hookFuture + .compose(hook -> executeHook(hook, group.getTimeout(), initialGroupResult, hookId)); groupFuture = groupFuture.compose(groupResult -> applyInvocationResult(invocationResult, hookId, startTime, groupResult)); @@ -95,23 +98,21 @@ public Future> execute() { return groupFuture.recover(GroupExecutor::restoreResultFromRejection); } - private Future> executeHook( - Hook hook, - Long timeout, - GroupResult groupResult, - HookId hookId) { - - if (hook == null) { - conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d); - - return Future.failedFuture(new FailedException("Hook implementation does not exist or disabled")); + private Future> hook(HookId hookId) { + try { + return Future.succeededFuture(hookProvider.apply(hookId)); + } catch (Exception e) { + return Future.failedFuture(new FailedException(e.getMessage())); } + } + + private Future> executeHook(Hook hook, + Long timeout, + GroupResult groupResult, + HookId hookId) { - return executeWithTimeout( - () -> hook.call( - groupResult.payload(), - invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId))), - timeout); + final CONTEXT invocationContext = invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId)); + return executeWithTimeout(() -> hook.call(groupResult.payload(), invocationContext), timeout); } private Future executeWithTimeout(Supplier> action, Long timeout) { diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java index a4487e3a60b..8bc7c5c0723 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java @@ -173,6 +173,7 @@ private static ExecutionAction toExecutionAction(InvocationAction action) { case reject -> ExecutionAction.reject; case update -> ExecutionAction.update; case no_action -> ExecutionAction.no_action; + case no_invocation -> ExecutionAction.no_invocation; }; } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java index 754e3925b11..f58b7f136c7 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java @@ -1,38 +1,46 @@ package org.prebid.server.hooks.execution; +import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.StageWithHookType; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.Module; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.util.Collection; import java.util.Objects; -/** - * Provides simple access to all {@link Hook}s registered in application. - */ public class HookCatalog { + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(HookCatalog.class)); + private final Collection modules; public HookCatalog(Collection modules) { this.modules = Objects.requireNonNull(modules); } - public > HOOK hookById( - String moduleCode, - String hookImplCode, - StageWithHookType stage) { + public > HOOK hookById(HookId hookId, + StageWithHookType stage) { final Class clazz = stage.hookType(); return modules.stream() - .filter(module -> Objects.equals(module.code(), moduleCode)) + .filter(module -> Objects.equals(module.code(), hookId.getModuleCode())) .map(Module::hooks) .flatMap(Collection::stream) - .filter(hook -> Objects.equals(hook.code(), hookImplCode)) + .filter(hook -> Objects.equals(hook.code(), hookId.getHookImplCode())) .filter(clazz::isInstance) .map(clazz::cast) .findFirst() - .orElse(null); + .orElseThrow(() -> { + logAbsentHook(hookId); + return new IllegalArgumentException("Hook implementation does not exist or disabled"); + }); + } + + private static void logAbsentHook(HookId hookId) { + conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d); } } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java index 81f44e3a528..cdb946f8d37 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -1,18 +1,24 @@ package org.prebid.server.hooks.execution; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.Vertx; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.collections4.map.DefaultedMap; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ABTest; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; @@ -22,6 +28,8 @@ import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.execution.provider.abtest.ABTestHookProvider; import org.prebid.server.hooks.execution.v1.InvocationContextImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -31,6 +39,7 @@ import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; @@ -41,24 +50,30 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountHooksConfiguration; +import org.prebid.server.settings.model.HooksAdminConfig; import java.time.Clock; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; public class HookStageExecutor { private static final String ENTITY_HTTP_REQUEST = "http-request"; + private static final String ENTITY_HTTP_RESPONSE = "http-response"; private static final String ENTITY_AUCTION_REQUEST = "auction-request"; private static final String ENTITY_AUCTION_RESPONSE = "auction-response"; private static final String ENTITY_ALL_PROCESSED_BID_RESPONSES = "all-processed-bid-responses"; @@ -66,17 +81,23 @@ public class HookStageExecutor { private final ExecutionPlan hostExecutionPlan; private final ExecutionPlan defaultAccountExecutionPlan; + private final Map hostModuleExecution; private final HookCatalog hookCatalog; private final TimeoutFactory timeoutFactory; private final Vertx vertx; private final Clock clock; + private final ObjectMapper mapper; + private final boolean isConfigToInvokeRequired; private HookStageExecutor(ExecutionPlan hostExecutionPlan, ExecutionPlan defaultAccountExecutionPlan, + Map hostModuleExecution, HookCatalog hookCatalog, TimeoutFactory timeoutFactory, Vertx vertx, - Clock clock) { + Clock clock, + ObjectMapper mapper, + boolean isConfigToInvokeRequired) { this.hostExecutionPlan = hostExecutionPlan; this.defaultAccountExecutionPlan = defaultAccountExecutionPlan; @@ -84,26 +105,76 @@ private HookStageExecutor(ExecutionPlan hostExecutionPlan, this.timeoutFactory = timeoutFactory; this.vertx = vertx; this.clock = clock; + this.mapper = mapper; + this.isConfigToInvokeRequired = isConfigToInvokeRequired; + this.hostModuleExecution = hostModuleExecution; } public static HookStageExecutor create(String hostExecutionPlan, String defaultAccountExecutionPlan, + Map hostModuleExecution, HookCatalog hookCatalog, TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + boolean isConfigToInvokeRequired) { + + Objects.requireNonNull(hookCatalog); + Objects.requireNonNull(mapper); return new HookStageExecutor( - parseAndValidateExecutionPlan( - hostExecutionPlan, - Objects.requireNonNull(mapper), - Objects.requireNonNull(hookCatalog)), + parseAndValidateExecutionPlan(hostExecutionPlan, mapper, hookCatalog), parseAndValidateExecutionPlan(defaultAccountExecutionPlan, mapper, hookCatalog), + hostModuleExecution, hookCatalog, Objects.requireNonNull(timeoutFactory), Objects.requireNonNull(vertx), - Objects.requireNonNull(clock)); + Objects.requireNonNull(clock), + mapper.mapper(), + isConfigToInvokeRequired); + } + + private static ExecutionPlan parseAndValidateExecutionPlan(String executionPlan, + JacksonMapper mapper, + HookCatalog hookCatalog) { + + return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog); + } + + private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { + if (StringUtils.isBlank(executionPlan)) { + return ExecutionPlan.empty(); + } + + try { + return mapper.decodeValue(executionPlan, ExecutionPlan.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); + } + } + + private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) { + plan.getEndpoints().values().stream() + .map(EndpointExecutionPlan::getStages) + .map(Map::entrySet) + .flatMap(Collection::stream) + .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream() + .map(ExecutionGroup::getHookSequence) + .flatMap(Collection::stream) + .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); + + return plan; + } + + private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { + try { + hookCatalog.hookById(hookId, StageWithHookType.forStage(stage)); + } catch (Throwable e) { + throw new IllegalArgumentException( + "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s" + .formatted(stage, hookId)); + } } public Future> executeEntrypointStage( @@ -116,8 +187,10 @@ public Future> executeEntrypointStag return stageExecutor(StageWithHookType.ENTRYPOINT, ENTITY_HTTP_REQUEST, context) .withExecutionPlan(planForEntrypointStage(endpoint)) + .withHookProvider(hookProviderForEntrypointStage(context)) .withInitialPayload(EntrypointPayloadImpl.of(queryParams, headers, body)) .withInvocationContextProvider(invocationContextProvider(endpoint)) + .withModulesExecution(DefaultedMap.defaultedMap(hostModuleExecution, true)) .withRejectAllowed(true) .execute(); } @@ -249,12 +322,28 @@ public Future> executeAuctionRe .execute(); } + public Future> executeExitpointStage(MultiMap responseHeaders, + String responseBody, + AuctionContext auctionContext) { + + final Account account = ObjectUtils.defaultIfNull(auctionContext.getAccount(), EMPTY_ACCOUNT); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final Endpoint endpoint = context.getEndpoint(); + + return stageExecutor(StageWithHookType.EXITPOINT, ENTITY_HTTP_RESPONSE, context, account, endpoint) + .withInitialPayload(ExitpointPayloadImpl.of(responseHeaders, responseBody)) + .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) + .withRejectAllowed(false) + .execute(); + } + private StageExecutor stageExecutor( StageWithHookType> stage, String entity, HookExecutionContext context) { - return StageExecutor.create(hookCatalog, vertx, clock) + return StageExecutor.create(vertx, clock) .withStage(stage) .withEntity(entity) .withHookExecutionContext(context); @@ -268,53 +357,30 @@ private StageExecutor stageToPlan.getValue().getGroups().stream() - .map(ExecutionGroup::getHookSequence) - .flatMap(Collection::stream) - .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); - - return plan; + .withModulesExecution(modulesExecutionForAccount(account)) + .withExecutionPlan(planForStage(account, endpoint, stage.stage())) + .withHookProvider(hookProvider(stage, account, context)); } - private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { - final Hook hook = hookCatalog.hookById( - hookId.getModuleCode(), - hookId.getHookImplCode(), - StageWithHookType.forStage(stage)); - - if (hook == null) { - throw new IllegalArgumentException( - "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s" - .formatted(stage, hookId)); + private Map modulesExecutionForAccount(Account account) { + final Map accountModulesExecution = Optional.ofNullable(account.getHooks()) + .map(AccountHooksConfiguration::getAdmin) + .map(HooksAdminConfig::getModuleExecution) + .orElse(Collections.emptyMap()); + + final Map resultModulesExecution = new HashMap<>(accountModulesExecution); + + if (isConfigToInvokeRequired) { + Optional.ofNullable(account.getHooks()) + .map(AccountHooksConfiguration::getModules) + .map(Map::keySet) + .stream() + .flatMap(Collection::stream) + .forEach(module -> resultModulesExecution.computeIfAbsent(module, key -> true)); } - } - private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { - if (StringUtils.isBlank(executionPlan)) { - return ExecutionPlan.empty(); - } - - try { - return mapper.decodeValue(executionPlan, ExecutionPlan.class); - } catch (DecodeException e) { - throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); - } + resultModulesExecution.putAll(hostModuleExecution); + return DefaultedMap.defaultedMap(resultModulesExecution, !isConfigToInvokeRequired); } private StageExecutionPlan planForEntrypointStage(Endpoint endpoint) { @@ -361,6 +427,34 @@ private ExecutionPlan effectiveExecutionPlanFor(Account account) { return accountExecutionPlan != null ? accountExecutionPlan : defaultAccountExecutionPlan; } + private HookProvider hookProviderForEntrypointStage( + HookExecutionContext context) { + + return new ABTestHookProvider<>( + defaultHookProvider(StageWithHookType.ENTRYPOINT), + abTestsForEntrypointStage(), + context, + mapper); + } + + private HookProvider hookProvider( + StageWithHookType> stage, + Account account, + HookExecutionContext context) { + + return new ABTestHookProvider<>( + defaultHookProvider(stage), + abTests(account), + context, + mapper); + } + + private HookProvider defaultHookProvider( + StageWithHookType> stage) { + + return hookId -> hookCatalog.hookById(hookId, stage); + } + private InvocationContextProvider invocationContextProvider(Endpoint endpoint) { return (timeout, hookId, moduleContext) -> invocationContext(endpoint, timeout); } @@ -406,10 +500,47 @@ private Timeout createTimeout(Long timeout) { } private static ObjectNode accountConfigFor(Account account, HookId hookId) { - final AccountHooksConfiguration accountHooksConfiguration = account.getHooks(); + final AccountHooksConfiguration accountHooksConfiguration = account != null ? account.getHooks() : null; final Map modulesConfiguration = accountHooksConfiguration != null ? accountHooksConfiguration.getModules() : Collections.emptyMap(); return modulesConfiguration != null ? modulesConfiguration.get(hookId.getModuleCode()) : null; } + + protected List abTestsForEntrypointStage() { + return ListUtils.emptyIfNull(hostExecutionPlan.getAbTests()).stream() + .filter(HookStageExecutor::isABTestEnabled) + .toList(); + } + + private static boolean isABTestEnabled(ABTest abTest) { + return abTest != null && abTest.isEnabled(); + } + + protected List abTests(Account account) { + return abTestsFromAccount(account) + .or(() -> abTestsFromHostConfig(account.getId())) + .orElse(Collections.emptyList()); + } + + private Optional> abTestsFromAccount(Account account) { + return Optional.of(effectiveExecutionPlanFor(account)) + .map(ExecutionPlan::getAbTests) + .map(abTests -> abTests.stream() + .filter(HookStageExecutor::isABTestEnabled) + .toList()); + } + + private Optional> abTestsFromHostConfig(String accountId) { + return Optional.ofNullable(hostExecutionPlan.getAbTests()) + .map(abTests -> abTests.stream() + .filter(HookStageExecutor::isABTestEnabled) + .filter(abTest -> isABTestApplicable(abTest, accountId)) + .toList()); + } + + private static boolean isABTestApplicable(ABTest abTest, String account) { + final Set accounts = abTest.getAccounts(); + return CollectionUtils.isEmpty(accounts) || accounts.contains(account); + } } diff --git a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java index f4f4a8176de..8dfa03e9a7f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java @@ -7,38 +7,39 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.provider.HookProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import java.time.Clock; import java.util.ArrayList; +import java.util.Map; class StageExecutor { - private final HookCatalog hookCatalog; private final Vertx vertx; private final Clock clock; private StageWithHookType> stage; private String entity; private StageExecutionPlan executionPlan; + private HookProvider hookProvider; private PAYLOAD initialPayload; private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; + private Map modulesExecution; - private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) { - this.hookCatalog = hookCatalog; + private StageExecutor(Vertx vertx, Clock clock) { this.vertx = vertx; this.clock = clock; } public static StageExecutor create( - HookCatalog hookCatalog, Vertx vertx, Clock clock) { - return new StageExecutor<>(hookCatalog, vertx, clock); + return new StageExecutor<>(vertx, clock); } public StageExecutor withStage(StageWithHookType> stage) { @@ -56,6 +57,11 @@ public StageExecutor withExecutionPlan(StageExecutionPlan exec return this; } + public StageExecutor withHookProvider(HookProvider hookProvider) { + this.hookProvider = hookProvider; + return this; + } + public StageExecutor withInitialPayload(PAYLOAD initialPayload) { this.initialPayload = initialPayload; return this; @@ -78,6 +84,11 @@ public StageExecutor withRejectAllowed(boolean rejectAllowed) return this; } + public StageExecutor withModulesExecution(Map modulesExecution) { + this.modulesExecution = modulesExecution; + return this; + } + public Future> execute() { Future> stageFuture = Future.succeededFuture(StageResult.of(initialPayload, entity)); @@ -94,11 +105,10 @@ public Future> execute() { } private Future> executeGroup(ExecutionGroup group, PAYLOAD initialPayload) { - return GroupExecutor.create(vertx, clock) + return GroupExecutor.create(vertx, clock, modulesExecution) .withGroup(group) .withInitialPayload(initialPayload) - .withHookProvider( - hookId -> hookCatalog.hookById(hookId.getModuleCode(), hookId.getHookImplCode(), stage)) + .withHookProvider(hookProvider) .withInvocationContextProvider(invocationContextProvider) .withHookExecutionContext(hookExecutionContext) .withRejectAllowed(rejectAllowed) diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java new file mode 100644 index 00000000000..67d64190101 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.execution.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Set; + +@Builder +@Value +public class ABTest { + + boolean enabled; + + @JsonProperty("module-code") + @JsonAlias("module_code") + String moduleCode; + + Set accounts; + + @JsonProperty("percent-active") + @JsonAlias("percent_active") + Integer percentActive; + + @JsonProperty("log-analytics-tag") + @JsonAlias("log_analytics_tag") + Boolean logAnalyticsTag; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java index 5e13aa3f14c..886cec114e8 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java @@ -2,5 +2,5 @@ public enum ExecutionAction { - no_action, update, reject + no_action, update, reject, no_invocation } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java index 5d0af8b8e23..8d865c26a90 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java @@ -1,15 +1,20 @@ package org.prebid.server.hooks.execution.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import org.prebid.server.model.Endpoint; import java.util.Collections; +import java.util.List; import java.util.Map; @Value(staticConstructor = "of") public class ExecutionPlan { - private static final ExecutionPlan EMPTY = of(Collections.emptyMap()); + private static final ExecutionPlan EMPTY = of(null, Collections.emptyMap()); + + @JsonProperty("abtests") + List abTests; Map endpoints; diff --git a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java index 47896d8c9ab..bb7c151ed6f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java @@ -33,5 +33,7 @@ public enum Stage { @JsonProperty("auction-response") @JsonAlias("auction_response") - auction_response + auction_response, + + exitpoint } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java index f8738d2c2db..961450a3c3f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java @@ -10,6 +10,7 @@ import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; public interface StageWithHookType> { @@ -29,6 +30,8 @@ public interface StageWithHookType(Stage.all_processed_bid_responses, AllProcessedBidResponsesHook.class); StageWithHookType AUCTION_RESPONSE = new StageWithHookTypeImpl<>(Stage.auction_response, AuctionResponseHook.class); + StageWithHookType EXITPOINT = + new StageWithHookTypeImpl<>(Stage.exitpoint, ExitpointHook.class); Stage stage(); @@ -44,6 +47,7 @@ public interface StageWithHookType ALL_PROCESSED_BID_RESPONSES; case processed_bidder_response -> PROCESSED_BIDDER_RESPONSE; case auction_response -> AUCTION_RESPONSE; + case exitpoint -> EXITPOINT; }; } } diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java new file mode 100644 index 00000000000..83297c26396 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.execution.provider; + +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.function.Function; + +public interface HookProvider + extends Function> { +} diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java new file mode 100644 index 00000000000..e7b82803f9a --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java @@ -0,0 +1,148 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ABTestHook implements Hook { + + private static final String ANALYTICS_ACTIVITY_NAME = "core-module-abtests"; + + private final String moduleName; + private final Hook hook; + private final boolean shouldInvokeHook; + private final boolean logABTestAnalyticsTag; + private final ObjectMapper mapper; + + public ABTestHook(String moduleName, + Hook hook, + boolean shouldInvokeHook, + boolean logABTestAnalyticsTag, + ObjectMapper mapper) { + + this.moduleName = Objects.requireNonNull(moduleName); + this.hook = Objects.requireNonNull(hook); + this.shouldInvokeHook = shouldInvokeHook; + this.logABTestAnalyticsTag = logABTestAnalyticsTag; + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public String code() { + return hook.code(); + } + + @Override + public Future> call(PAYLOAD payload, CONTEXT invocationContext) { + if (!shouldInvokeHook) { + return skippedResult(); + } + + final Future> invocationResultFuture = hook.call(payload, invocationContext); + return logABTestAnalyticsTag + ? invocationResultFuture.map(this::enrichWithABTestAnalyticsTag) + : invocationResultFuture; + } + + private Future> skippedResult() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_invocation) + .analyticsTags(logABTestAnalyticsTag ? tags("skipped") : null) + .build()); + } + + private Tags tags(String status) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ANALYTICS_ACTIVITY_NAME, + "success", + Collections.singletonList(ResultImpl.of(status, analyticsValues(), null))))); + } + + private ObjectNode analyticsValues() { + final ObjectNode values = mapper.createObjectNode(); + values.put("module", moduleName); + return values; + } + + private InvocationResult enrichWithABTestAnalyticsTag(InvocationResult invocationResult) { + return new InvocationResultWithAdditionalTags<>(invocationResult, tags("run")); + } + + private record InvocationResultWithAdditionalTags(InvocationResult invocationResult, + Tags additionalTags) + implements InvocationResult { + + @Override + public InvocationStatus status() { + return invocationResult.status(); + } + + @Override + public String message() { + return invocationResult.message(); + } + + @Override + public InvocationAction action() { + return invocationResult.action(); + } + + @Override + public PayloadUpdate payloadUpdate() { + return invocationResult.payloadUpdate(); + } + + @Override + public List errors() { + return invocationResult.errors(); + } + + @Override + public List warnings() { + return invocationResult.warnings(); + } + + @Override + public List debugMessages() { + return invocationResult.debugMessages(); + } + + @Override + public Object moduleContext() { + return invocationResult.moduleContext(); + } + + @Override + public Tags analyticsTags() { + return new TagsUnion(invocationResult.analyticsTags(), additionalTags); + } + } + + private record TagsUnion(Tags left, Tags right) implements Tags { + + @Override + public List activities() { + return left != null + ? ListUtil.union(left.activities(), right.activities()) + : right.activities(); + } + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java new file mode 100644 index 00000000000..6a833ab2833 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java @@ -0,0 +1,87 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.hooks.execution.model.ABTest; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; + +public class ABTestHookProvider implements HookProvider { + + private final HookProvider innerHookProvider; + private final List abTests; + private final HookExecutionContext context; + private final ObjectMapper mapper; + + public ABTestHookProvider(HookProvider innerHookProvider, + List abTests, + HookExecutionContext context, + ObjectMapper mapper) { + + this.innerHookProvider = Objects.requireNonNull(innerHookProvider); + this.abTests = Objects.requireNonNull(abTests); + this.context = Objects.requireNonNull(context); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Hook apply(HookId hookId) { + final Hook hook = innerHookProvider.apply(hookId); + + final String moduleCode = hookId.getModuleCode(); + final ABTest abTest = searchForABTest(moduleCode); + if (abTest == null) { + return hook; + } + + return new ABTestHook<>( + moduleCode, + hook, + shouldInvokeHook(moduleCode, abTest), + BooleanUtils.isNotFalse(abTest.getLogAnalyticsTag()), + mapper); + } + + private ABTest searchForABTest(String moduleCode) { + return abTests.stream() + .filter(abTest -> moduleCode.equals(abTest.getModuleCode())) + .findFirst() + .orElse(null); + } + + protected boolean shouldInvokeHook(String moduleCode, ABTest abTest) { + final HookExecutionOutcome hookExecutionOutcome = searchForPreviousExecution(moduleCode); + if (hookExecutionOutcome != null) { + return hookExecutionOutcome.getAction() != ExecutionAction.no_invocation; + } + + final int percent = ObjectUtils.defaultIfNull(abTest.getPercentActive(), 100); + return ThreadLocalRandom.current().nextInt(100) < percent; + } + + private HookExecutionOutcome searchForPreviousExecution(String moduleCode) { + return context.getStageOutcomes().values().stream() + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookExecutionOutcome -> hookExecutionOutcome.getHookId().getModuleCode().equals(moduleCode)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java index 6ed23ef8980..99399d5ba6b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java @@ -2,7 +2,7 @@ import lombok.Value; import lombok.experimental.Accessors; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.model.Endpoint; diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java similarity index 91% rename from extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java index b77fc98a68b..761aef951ec 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model; +package org.prebid.server.hooks.execution.v1; import lombok.Builder; import lombok.Value; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java similarity index 82% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java index 484489a5e6f..4c9747e16bc 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import lombok.Value; import lombok.experimental.Accessors; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java similarity index 83% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java index 2971cc40d6e..884603b4717 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import lombok.Builder; import lombok.Value; @@ -8,8 +8,8 @@ import java.util.List; @Accessors(fluent = true) -@Value @Builder +@Value public class AppliedToImpl implements AppliedTo { List impIds; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java similarity index 84% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java index 5405799e25f..c16397e894c 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java similarity index 81% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java index 9f0432b9e2f..f068f28dcef 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import lombok.Value; import lombok.experimental.Accessors; diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java new file mode 100644 index 00000000000..d57080f6b90 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution.v1.exitpoint; + +import io.vertx.core.MultiMap; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ExitpointPayloadImpl implements ExitpointPayload { + + MultiMap responseHeaders; + + String responseBody; +} diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java index 29b22bf1b3d..821b21a730b 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java @@ -2,5 +2,5 @@ public enum InvocationAction { - no_action, update, reject + no_action, update, reject, no_invocation } diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java index 7c3b6c922d3..22493ea8a07 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java @@ -1,6 +1,6 @@ package org.prebid.server.hooks.v1; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.model.Endpoint; public interface InvocationContext { diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java new file mode 100644 index 00000000000..02e36af17a5 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.exitpoint; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +public interface ExitpointHook extends Hook { +} diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java new file mode 100644 index 00000000000..ae596949fa0 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.v1.exitpoint; + +import io.vertx.core.MultiMap; + +public interface ExitpointPayload { + + MultiMap responseHeaders(); + + String responseBody(); +} diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index fc9d3c251c3..9ea3d15f0fc 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -25,6 +25,7 @@ public enum MetricName { // auction requests, + debug_requests, app_requests, no_cookie_requests, request_time, @@ -138,6 +139,7 @@ public enum MetricName { call, success, noop, + no_invocation("no-invocation"), reject, unknown, failure, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index ed11d511f5f..52964a9f8b0 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -167,6 +167,12 @@ HooksMetrics hooks() { return hooksMetrics; } + public void updateDebugRequestMetrics(boolean debugEnabled) { + if (debugEnabled) { + incCounter(MetricName.debug_requests); + } + } + public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, int numImps) { if (isApp) { incCounter(MetricName.app_requests); @@ -235,12 +241,20 @@ public void updateAccountRequestMetrics(Account account, MetricName requestType) final AccountMetrics accountMetrics = forAccount(account.getId()); accountMetrics.incCounter(MetricName.requests); + if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed)) { accountMetrics.requestType(requestType).incCounter(MetricName.requests); } } } + public void updateAccountDebugRequestMetrics(Account account, boolean debugEnabled) { + final AccountMetricsVerbosityLevel verbosityLevel = accountMetricsVerbosityResolver.forAccount(account); + if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed) && debugEnabled) { + forAccount(account.getId()).incCounter(MetricName.debug_requests); + } + } + public void updateAccountRequestRejectedByInvalidAccountMetrics(String accountId) { updateAccountRequestsMetrics(accountId, MetricName.rejected_by_invalid_account); } @@ -614,13 +628,20 @@ public void updateHooksMetrics( final HookImplMetrics hookImplMetrics = hooks().module(moduleCode).stage(stage).hookImpl(hookImplCode); - hookImplMetrics.incCounter(MetricName.call); + if (action != ExecutionAction.no_invocation) { + hookImplMetrics.incCounter(MetricName.call); + } + if (status == ExecutionStatus.success) { hookImplMetrics.success().incCounter(HookMetricMapper.fromAction(action)); } else { hookImplMetrics.incCounter(HookMetricMapper.fromStatus(status)); } - hookImplMetrics.updateTimer(MetricName.duration, executionTime); + + if (action != ExecutionAction.no_invocation) { + hookImplMetrics.updateTimer(MetricName.duration, executionTime); + } + } public void updateAccountHooksMetrics( @@ -632,7 +653,10 @@ public void updateAccountHooksMetrics( if (accountMetricsVerbosityResolver.forAccount(account).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { final ModuleMetrics accountModuleMetrics = forAccount(account.getId()).hooks().module(moduleCode); - accountModuleMetrics.incCounter(MetricName.call); + if (action != ExecutionAction.no_invocation) { + accountModuleMetrics.incCounter(MetricName.call); + } + if (status == ExecutionStatus.success) { accountModuleMetrics.success().incCounter(HookMetricMapper.fromAction(action)); } else { @@ -663,6 +687,7 @@ private static class HookMetricMapper { ACTION_TO_METRIC.put(ExecutionAction.no_action, MetricName.noop); ACTION_TO_METRIC.put(ExecutionAction.update, MetricName.update); ACTION_TO_METRIC.put(ExecutionAction.reject, MetricName.reject); + ACTION_TO_METRIC.put(ExecutionAction.no_invocation, MetricName.no_invocation); } static MetricName fromStatus(ExecutionStatus status) { diff --git a/src/main/java/org/prebid/server/metric/StageMetrics.java b/src/main/java/org/prebid/server/metric/StageMetrics.java index 1cc8f3adfb3..025a47368bd 100644 --- a/src/main/java/org/prebid/server/metric/StageMetrics.java +++ b/src/main/java/org/prebid/server/metric/StageMetrics.java @@ -21,6 +21,8 @@ class StageMetrics extends UpdatableMetrics { STAGE_TO_METRIC.put(Stage.raw_bidder_response, "rawbidresponse"); STAGE_TO_METRIC.put(Stage.processed_bidder_response, "procbidresponse"); STAGE_TO_METRIC.put(Stage.auction_response, "auctionresponse"); + STAGE_TO_METRIC.put(Stage.all_processed_bid_responses, "allprocbidresponses"); + STAGE_TO_METRIC.put(Stage.exitpoint, "exitpoint"); } private static final String UNKNOWN_STAGE = "unknown"; diff --git a/src/main/java/org/prebid/server/model/HttpRequestContext.java b/src/main/java/org/prebid/server/model/HttpRequestContext.java index efa07ed621e..9237e9b1803 100644 --- a/src/main/java/org/prebid/server/model/HttpRequestContext.java +++ b/src/main/java/org/prebid/server/model/HttpRequestContext.java @@ -2,6 +2,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import lombok.Builder; import lombok.Value; @@ -16,6 +17,8 @@ @Value public class HttpRequestContext { + HttpMethod httpMethod; + String absoluteUri; CaseInsensitiveMultiMap queryParams; @@ -30,6 +33,7 @@ public class HttpRequestContext { public static HttpRequestContext from(RoutingContext context) { return HttpRequestContext.builder() + .httpMethod(context.request().method()) .absoluteUri(context.request().uri()) .queryParams(CaseInsensitiveMultiMap.builder().addAll(toMap(context.request().params())).build()) .headers(headers(context)) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 8836fbaa24c..5c994ec590f 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -10,7 +10,7 @@ import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; @@ -66,6 +66,7 @@ public class TcfDefinerService { private final BidderCatalog bidderCatalog; private final IpAddressHelper ipAddressHelper; private final Metrics metrics; + private final double samplingRate; public TcfDefinerService(GdprConfig gdprConfig, Set eeaCountries, @@ -73,7 +74,8 @@ public TcfDefinerService(GdprConfig gdprConfig, GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, - Metrics metrics) { + Metrics metrics, + double samplingRate) { this.gdprEnabled = gdprConfig != null && BooleanUtils.isNotFalse(gdprConfig.getEnabled()); this.gdprDefaultValue = gdprConfig != null ? gdprConfig.getDefaultValue() : null; @@ -85,6 +87,7 @@ public TcfDefinerService(GdprConfig gdprConfig, this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; } /** @@ -360,11 +363,14 @@ private TCStringParsingResult toValidResult(String consentString, TCStringParsin } final int tcfPolicyVersion = tcString.getTcfPolicyVersion(); - // disable support for tcf policy version > 5 + // support for tcf policy version > 5 if (tcfPolicyVersion > 5) { - warnings.add("Parsing consent string: %s failed. TCF policy version %d is not supported".formatted( - consentString, tcfPolicyVersion)); - return TCStringParsingResult.of(TCStringEmpty.create(), warnings); + metrics.updateAlertsMetrics(MetricName.general); + + final String message = "Unknown tcfPolicyVersion %s, defaulting to gvlSpecificationVersion=3" + .formatted(tcfPolicyVersion); + UNDEFINED_CORRUPT_CONSENT_LOGGER.warn(message, samplingRate); + warnings.add(message); } return TCStringParsingResult.of(tcString, warnings); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java index d0b6057e41a..5e261d9b6b4 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java @@ -2,7 +2,6 @@ import com.iabtcf.decoder.TCString; import io.vertx.core.Future; -import org.prebid.server.exception.PreBidException; import org.prebid.server.privacy.gdpr.vendorlist.proto.Vendor; import java.util.Map; @@ -21,10 +20,6 @@ public VersionedVendorListService(VendorListService vendorListServiceV2, VendorL public Future> forConsent(TCString consent) { final int tcfPolicyVersion = consent.getTcfPolicyVersion(); final int vendorListVersion = consent.getVendorListVersion(); - if (tcfPolicyVersion > 5) { - return Future.failedFuture(new PreBidException( - "Invalid tcf policy version: %d".formatted(tcfPolicyVersion))); - } return tcfPolicyVersion < 4 ? vendorListServiceV2.forVersion(vendorListVersion) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java index f80f925cd5f..fd6206cb90d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java @@ -56,4 +56,9 @@ public class ExtImpPrebid { * Defines the contract for bidrequest.imp[i].ext.prebid.passthrough */ JsonNode passthrough; + + /** + * Defines the contract for bidrequest.imp[i].ext.prebid.imp + */ + ObjectNode imp; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java new file mode 100644 index 00000000000..ab0565ce44e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Builder(toBuilder = true) +@Value +public class ExtRequestBidAdjustments { + + Map>>> mediatype; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java new file mode 100644 index 00000000000..a857575a85f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java @@ -0,0 +1,24 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; + +import java.math.BigDecimal; + +@Builder(toBuilder = true) +@Value +public class ExtRequestBidAdjustmentsRule { + + @JsonProperty("adjtype") + BidAdjustmentType adjType; + + BigDecimal value; + + String currency; + + public String toString() { + return "[adjtype=%s, value=%s, currency=%s]".formatted(adjType, value, currency); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 6dee1b7ba38..cb325bd088a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -50,6 +50,11 @@ public class ExtRequestPrebid { */ ExtRequestBidAdjustmentFactors bidadjustmentfactors; + /** + * Defines the contract for bidrequest.ext.prebid.bidadjustments + */ + ObjectNode bidadjustments; + /** * Defines the contract for bidrequest.ext.prebid.currency */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java index 852a106a7ef..e8f83afcccb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java @@ -15,4 +15,7 @@ public class ExtStoredAuctionResponse { @JsonProperty("seatbidarr") List seatBids; + + @JsonProperty("seatbidobj") + SeatBid seatBid; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java index 900a355a9fb..c570e221362 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java @@ -55,8 +55,13 @@ public class ExtUser extends FlexibleExtension { /** * Defines the contract for bidrequest.user.ext.ConsentedProvidersSettings + *

+ * TODO: Remove after PBS 4.0 */ + @Deprecated(forRemoval = true) @JsonProperty("ConsentedProvidersSettings") + ConsentedProvidersSettings deprecatedConsentedProvidersSettings; + ConsentedProvidersSettings consentedProvidersSettings; @JsonIgnore diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java index 732ddca6236..d619ed27e80 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java @@ -9,6 +9,8 @@ public enum ImpMediaType { @JsonProperty("native") xNative, video, + @JsonProperty("video-instream") + video_instream, @JsonProperty("video-outstream") video_outstream; @@ -16,6 +18,7 @@ public enum ImpMediaType { public String toString() { return this == xNative ? "native" : this == video_outstream ? "video-outstream" + : this == video_instream ? "video-instream" : super.toString(); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java index 04105dd73d8..268ccd077ec 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java @@ -1,39 +1,22 @@ package org.prebid.server.proto.openrtb.ext.request.adtarget; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -/** - * Defines the contract for bidrequest.imp[i].ext.adtarget - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdtarget { - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.aid - */ @JsonProperty("aid") - Integer sourceId; + String sourceId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.placementId - */ @JsonProperty("placementId") Integer placementId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.siteId - */ @JsonProperty("siteId") Integer siteId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.bidFloor - */ @JsonProperty("bidFloor") BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java index 907932dd44b..00df38b9533 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java @@ -1,39 +1,22 @@ package org.prebid.server.proto.openrtb.ext.request.adtelligent; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -/** - * Defines the contract for bidrequest.imp[i].ext.adtelligent - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdtelligent { - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.aid - */ @JsonProperty("aid") - Integer sourceId; + String sourceId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.placementId - */ @JsonProperty("placementId") Integer placementId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.siteId - */ @JsonProperty("siteId") Integer siteId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.bidFloor - */ @JsonProperty("bidFloor") BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java new file mode 100644 index 00000000000..121d025f654 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.adtonos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdtonos { + + @JsonProperty("supplierId") + String supplierId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java new file mode 100644 index 00000000000..5823f02551e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request.bidmatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpBidmatic { + + @JsonProperty("source") + String sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java similarity index 77% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java index dae1cd49c62..99413fa5e40 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java @@ -1,10 +1,10 @@ -package org.prebid.server.proto.openrtb.ext.request.bizzclick; +package org.prebid.server.proto.openrtb.ext.request.blasto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @Value(staticConstructor = "of") -public class ExtImpBizzclick { +public class ExtImpBlasto { @JsonProperty("host") String host; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java index fc5f6dc6489..a88bbc2194d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java @@ -11,10 +11,10 @@ public class ExtImpConnectAd { @JsonProperty("networkId") - Integer networkId; + String networkId; @JsonProperty("siteId") - Integer siteId; + String siteId; @JsonProperty("bidfloor") BigDecimal bidFloor; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java new file mode 100644 index 00000000000..1c2c057878a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.copper6ssp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ImpExtCopper6Ssp { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} + diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java new file mode 100644 index 00000000000..03b14ab82eb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.escalax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpEscalax { + + @JsonProperty("sourceId") + String sourceId; + + @JsonProperty("accountId") + String accountId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java index f572890ab72..b07a6741019 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java @@ -1,16 +1,18 @@ package org.prebid.server.proto.openrtb.ext.request.loopme; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -/** - * Defines the contract for bidrequest.imp[i].ext.loopme - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpLoopme { - @JsonProperty("accountId") - String accountId; + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("bundleId") + String bundleId; + + @JsonProperty("placementId") + String placementId; + } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java new file mode 100644 index 00000000000..ae7b45e8c86 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.melozen; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MeloZenImpExt { + + @JsonProperty("pubId") + String pubId; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java new file mode 100644 index 00000000000..3101cb8a214 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.metax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMetax { + + @JsonProperty("publisherId") + Integer publisherId; + + @JsonProperty("adunit") + Integer adUnit; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java new file mode 100644 index 00000000000..016e6ca99d5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMissena { + + @JsonProperty("apiKey") + String apiKey; + + String placement; + + @JsonProperty("test") + String testMode; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java new file mode 100644 index 00000000000..860ddb430d9 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.oraki; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOraki { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java new file mode 100644 index 00000000000..6206f540b92 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.ownadx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOwnAdx { + + @JsonProperty("sspId") + String sspId; + + @JsonProperty("seatId") + String seatId; + + @JsonProperty("tokenId") + String tokenId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java new file mode 100644 index 00000000000..6cac7640123 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.pubrise; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpPubrise { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java new file mode 100644 index 00000000000..0f5df5f144d --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.qt; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpQt { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java new file mode 100644 index 00000000000..ee3b9cc7d83 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.thetradedesk; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTheTradeDesk { + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java new file mode 100644 index 00000000000..5f20441f9cd --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.tradplus; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTradPlus { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java similarity index 60% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java index 5232d38c985..2b6fdfadae4 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java @@ -1,9 +1,9 @@ -package org.prebid.server.proto.openrtb.ext.request.liftoff; +package org.prebid.server.proto.openrtb.ext.request.vungle; import lombok.Value; @Value(staticConstructor = "of") -public class ExtImpLiftoff { +public class ExtImpVungle { String bidToken; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java index 28a4e036904..db165d6dd4e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java @@ -6,9 +6,6 @@ import java.util.Map; -/** - * Defines the contract for bidrequest.imp[i].ext.yieldlab - */ @Builder @Value public class ExtImpYieldlab { @@ -19,9 +16,6 @@ public class ExtImpYieldlab { @JsonProperty("supplyId") String supplyId; - @JsonProperty("adSize") - String adSize; - Map targeting; @JsonProperty("extId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java index aa24e176303..70683a8a7a7 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java @@ -1,36 +1,22 @@ package org.prebid.server.proto.openrtb.ext.response; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Value; import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import java.util.List; -/** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa - */ -@Value(staticConstructor = "of") +@Builder(toBuilder = true) +@Value public class ExtBidDsa { - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.behalf - */ String behalf; - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.paid - */ String paid; - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.transparency[] - */ List transparency; - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.adrender - */ @JsonProperty("adrender") Integer adRender; - } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java index b7295616a82..eaba36abca0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java @@ -67,4 +67,6 @@ public class ExtBidPrebidMeta { @JsonProperty("secondaryCatIds") List secondaryCategoryIdList; + String seat; + } diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index da414bef279..7a6582ccd42 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -1,7 +1,7 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 97348e7bbd8..9f8fcea9ff2 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -3,7 +3,7 @@ import io.vertx.core.Future; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; diff --git a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java index 2edd16b7345..32d47d6abad 100644 --- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java @@ -1,7 +1,7 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java index 9dad7f6a28b..c346e4824f4 100644 --- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -7,7 +7,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.helper.DatabaseStoredDataResultMapper; diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 11a0d2cb3af..bfde0fc2e81 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -4,7 +4,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index 33a1ea36390..1a2f42e86c4 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -8,7 +8,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 1c78e4693c5..98517003baf 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -9,7 +9,7 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java new file mode 100644 index 00000000000..f1c8b107c5f --- /dev/null +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -0,0 +1,227 @@ +package org.prebid.server.settings; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; +import software.amazon.awssdk.core.BytesWrapper; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Implementation of {@link ApplicationSettings}. + *

+ * Reads an application settings from JSON file in a s3 bucket, stores and serves them in and from the memory. + *

+ * Immediately loads stored request data from local files. These are stored in memory for low-latency reads. + * This expects each file in the directory to be named "{config_id}.json". + */ +public class S3ApplicationSettings implements ApplicationSettings { + + private static final String JSON_SUFFIX = ".json"; + + final S3AsyncClient asyncClient; + final String bucket; + final String accountsDirectory; + final String storedImpressionsDirectory; + final String storedRequestsDirectory; + final String storedResponsesDirectory; + final JacksonMapper jacksonMapper; + final Vertx vertx; + + public S3ApplicationSettings(S3AsyncClient asyncClient, + String bucket, + String accountsDirectory, + String storedImpressionsDirectory, + String storedRequestsDirectory, + String storedResponsesDirectory, + JacksonMapper jacksonMapper, + Vertx vertx) { + + this.asyncClient = Objects.requireNonNull(asyncClient); + this.bucket = Objects.requireNonNull(bucket); + this.accountsDirectory = Objects.requireNonNull(accountsDirectory); + this.storedImpressionsDirectory = Objects.requireNonNull(storedImpressionsDirectory); + this.storedRequestsDirectory = Objects.requireNonNull(storedRequestsDirectory); + this.storedResponsesDirectory = Objects.requireNonNull(storedResponsesDirectory); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.vertx = Objects.requireNonNull(vertx); + } + + @Override + public Future getAccountById(String accountId, Timeout timeout) { + return withTimeout(() -> downloadFile(accountsDirectory + "/" + accountId + JSON_SUFFIX), timeout) + .map(fileContent -> decodeAccount(fileContent, accountId)); + } + + private Account decodeAccount(String fileContent, String requestedAccountId) { + if (fileContent == null) { + throw new PreBidException("Account with id %s not found".formatted(requestedAccountId)); + } + + final Account account; + try { + account = jacksonMapper.decodeValue(fileContent, Account.class); + } catch (DecodeException e) { + throw new PreBidException("Invalid json for account with id %s".formatted(requestedAccountId)); + } + + validateAccount(account, requestedAccountId); + return account; + } + + private static void validateAccount(Account account, String requestedAccountId) { + final String receivedAccountId = account != null ? account.getId() : null; + if (!StringUtils.equals(receivedAccountId, requestedAccountId)) { + throw new PreBidException( + "Account with id %s does not match id %s in file".formatted(requestedAccountId, receivedAccountId)); + } + } + + @Override + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return withTimeout( + () -> Future.all( + getFileContents(storedRequestsDirectory, requestIds), + getFileContents(storedImpressionsDirectory, impIds)), + timeout) + .map(results -> buildStoredDataResult( + results.resultAt(0), + results.resultAt(1), + requestIds, + impIds)); + } + + private StoredDataResult buildStoredDataResult(Map storedIdToRequest, + Map storedIdToImp, + Set requestIds, + Set impIds) { + + final List errors = Stream.concat( + missingStoredDataIds(storedIdToImp, impIds).stream() + .map("No stored impression found for id: %s"::formatted), + missingStoredDataIds(storedIdToRequest, requestIds).stream() + .map("No stored request found for id: %s"::formatted)) + .toList(); + + return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); + } + + private Set missingStoredDataIds(Map fileContents, Set responseIds) { + return SetUtils.difference(responseIds, fileContents.keySet()); + } + + @Override + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + } + + @Override + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return withTimeout(() -> getFileContents(storedResponsesDirectory, responseIds), timeout) + .map(storedIdToResponse -> StoredResponseDataResult.of( + storedIdToResponse, + missingStoredDataIds(storedIdToResponse, responseIds).stream() + .map("No stored response found for id: %s"::formatted) + .toList())); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return Future.succeededFuture(Collections.emptyMap()); + } + + private Future> getFileContents(String directory, Set ids) { + return Future.join(ids.stream() + .map(impId -> downloadFile(directory + withInitialSlash(impId) + JSON_SUFFIX) + .map(fileContent -> Tuple2.of(impId, fileContent))) + .toList()) + .map(CompositeFuture::>list) + .map(impIdToFileContent -> impIdToFileContent.stream() + .filter(tuple -> tuple.getRight() != null) + .collect(Collectors.toMap(Tuple2::getLeft, Tuple2::getRight))); + } + + /** + * When the impression id is the ad unit path it may already start with a slash and there's no need to add + * another one. + * + * @param impressionId from the bid request + * @return impression id with only a single slash at the beginning + */ + private static String withInitialSlash(String impressionId) { + return impressionId.startsWith("/") ? impressionId : "/" + impressionId; + } + + private Future downloadFile(String key) { + final GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + return Future.fromCompletionStage( + asyncClient.getObject(request, AsyncResponseTransformer.toBytes()), + vertx.getOrCreateContext()) + .map(BytesWrapper::asUtf8String) + .otherwiseEmpty(); + } + + private Future withTimeout(Supplier> futureFactory, Timeout timeout) { + final long remainingTime = timeout.remaining(); + if (remainingTime <= 0L) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final Promise promise = Promise.promise(); + final Future future = futureFactory.get(); + + final long timerId = vertx.setTimer(remainingTime, id -> + promise.tryFail(new TimeoutException("Timeout has been exceeded"))); + + future.onComplete(result -> { + vertx.cancelTimer(timerId); + if (result.succeeded()) { + promise.tryComplete(result.result()); + } else { + promise.tryFail(result.cause()); + } + }); + + return promise.future(); + } +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index 9708c23bb1c..9943535aa7e 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; import org.prebid.server.spring.config.bidder.model.MediaType; @@ -33,6 +34,9 @@ public class AccountAuctionConfig { @JsonAlias("bid-validations") AccountBidValidationConfig bidValidations; + @JsonProperty("bidadjustments") + ObjectNode bidAdjustments; + AccountEventsConfig events; @JsonAlias("price-floors") diff --git a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java index 75d3b03b6e5..f45af371385 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java +++ b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java @@ -14,4 +14,6 @@ public class AccountHooksConfiguration { ExecutionPlan executionPlan; Map modules; + + HooksAdminConfig admin; } diff --git a/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java new file mode 100644 index 00000000000..36b12a401f0 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class HooksAdminConfig { + + @JsonAlias("module-execution") + Map moduleExecution; + +} diff --git a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java index cc4b80ad870..bdd9e8258e0 100644 --- a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java @@ -4,8 +4,8 @@ import io.vertx.core.Promise; import io.vertx.core.Vertx; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; diff --git a/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java new file mode 100644 index 00000000000..d5a8ce7f873 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java @@ -0,0 +1,146 @@ +package org.prebid.server.settings.service; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.CacheNotificationListener; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.vertx.Initializable; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.S3Object; + +import java.time.Clock; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * Service that periodically calls s3 for stored request updates. + * If refreshRate is negative, then the data will never be refreshed. + *

+ * Fetches all files from the specified folders/prefixes in s3 and downloads all files. + */ +public class S3PeriodicRefreshService implements Initializable { + + private static final String JSON_SUFFIX = ".json"; + + private static final Logger logger = LoggerFactory.getLogger(S3PeriodicRefreshService.class); + + private final S3AsyncClient asyncClient; + private final String bucket; + private final String storedRequestsDirectory; + private final String storedImpressionsDirectory; + private final long refreshPeriod; + private final CacheNotificationListener cacheNotificationListener; + private final MetricName cacheType; + private final Clock clock; + private final Metrics metrics; + private final Vertx vertx; + + public S3PeriodicRefreshService(S3AsyncClient asyncClient, + String bucket, + String storedRequestsDirectory, + String storedImpressionsDirectory, + long refreshPeriod, + CacheNotificationListener cacheNotificationListener, + MetricName cacheType, + Clock clock, + Metrics metrics, + Vertx vertx) { + + this.asyncClient = Objects.requireNonNull(asyncClient); + this.bucket = Objects.requireNonNull(bucket); + this.storedRequestsDirectory = Objects.requireNonNull(storedRequestsDirectory); + this.storedImpressionsDirectory = Objects.requireNonNull(storedImpressionsDirectory); + this.refreshPeriod = refreshPeriod; + this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener); + this.cacheType = Objects.requireNonNull(cacheType); + this.clock = Objects.requireNonNull(clock); + this.metrics = Objects.requireNonNull(metrics); + this.vertx = Objects.requireNonNull(vertx); + } + + @Override + public void initialize(Promise initializePromise) { + fetchStoredDataResult(clock.millis(), MetricName.initialize) + .mapEmpty() + .onComplete(initializePromise); + + if (refreshPeriod > 0) { + logger.info("Starting s3 periodic refresh for " + cacheType + " every " + refreshPeriod + " s"); + vertx.setPeriodic(refreshPeriod, ignored -> fetchStoredDataResult(clock.millis(), MetricName.update)); + } + } + + private Future fetchStoredDataResult(long startTime, MetricName metricName) { + return Future.all( + getFileContentsForDirectory(storedRequestsDirectory), + getFileContentsForDirectory(storedImpressionsDirectory)) + .map(CompositeFuture::>list) + .map(results -> StoredDataResult.of(results.getFirst(), results.get(1), Collections.emptyList())) + .onSuccess(storedDataResult -> handleResult(storedDataResult, startTime, metricName)) + .onFailure(exception -> handleFailure(exception, startTime, metricName)); + } + + private Future> getFileContentsForDirectory(String directory) { + return listFiles(directory) + .map(files -> files.stream().map(this::downloadFile).toList()) + .compose(Future::all) + .map(CompositeFuture::>list) + .map(fileNameToContent -> fileNameToContent.stream() + .collect(Collectors.toMap( + entry -> stripFileName(directory, entry.getLeft()), + Tuple2::getRight))); + } + + private Future> listFiles(String prefix) { + final ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder() + .bucket(bucket) + .prefix(prefix) + .build(); + + return Future.fromCompletionStage(asyncClient.listObjects(listObjectsRequest), vertx.getOrCreateContext()) + .map(response -> response.contents().stream() + .map(S3Object::key) + .collect(Collectors.toList())); + } + + private Future> downloadFile(String key) { + final GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + return Future.fromCompletionStage( + asyncClient.getObject(request, AsyncResponseTransformer.toBytes()), + vertx.getOrCreateContext()) + .map(content -> Tuple2.of(key, content.asUtf8String())); + } + + private static String stripFileName(String directory, String name) { + return name + .replace(directory + "/", "") + .replace(JSON_SUFFIX, ""); + } + + private void handleResult(StoredDataResult storedDataResult, long startTime, MetricName refreshType) { + cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp()); + metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); + } + + private void handleFailure(Throwable exception, long startTime, MetricName refreshType) { + logger.warn("Error occurred while request to s3 refresh service", exception); + + metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); + metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java index 688cb0c5efb..d618c36fa36 100644 --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java @@ -4,8 +4,12 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.analytics.reporter.agma.AgmaAnalyticsReporter; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; import org.prebid.server.analytics.reporter.greenbids.GreenbidsAnalyticsReporter; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; import org.prebid.server.analytics.reporter.log.LogAnalyticsReporter; @@ -25,9 +29,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.util.List; +import java.util.Set; +import java.util.Map; +import java.util.stream.Collectors; @Configuration public class AnalyticsConfiguration { @@ -39,7 +47,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( TcfEnforcement tcfEnforcement, UserFpdActivityMask userFpdActivityMask, Metrics metrics, - @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + @Value("${logging.sampling-rate:0.01}") double logSamplingRate, + @Value("${analytics.global.adapters}") Set globalEnabledAdapters, + JacksonMapper mapper) { return new AnalyticsReporterDelegator( vertx, @@ -47,7 +57,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( tcfEnforcement, userFpdActivityMask, metrics, - logSamplingRate); + logSamplingRate, + globalEnabledAdapters, + mapper); } @Bean @@ -56,6 +68,115 @@ LogAnalyticsReporter logAnalyticsReporter(JacksonMapper mapper) { return new LogAnalyticsReporter(mapper); } + @Configuration + @ConditionalOnProperty(prefix = "analytics.agma", name = "enabled", havingValue = "true") + public static class AgmaAnalyticsConfiguration { + + @Bean + AgmaAnalyticsReporter agmaAnalyticsReporter(AgmaAnalyticsConfigurationProperties properties, + JacksonMapper jacksonMapper, + HttpClient httpClient, + Clock clock, + PrebidVersionProvider prebidVersionProvider, + Vertx vertx) { + + return new AgmaAnalyticsReporter( + properties.toComponentProperties(), + prebidVersionProvider, + jacksonMapper, + clock, + httpClient, + vertx); + } + + @Bean + @ConfigurationProperties(prefix = "analytics.agma") + AgmaAnalyticsConfigurationProperties agmaAnalyticsConfigurationProperties() { + return new AgmaAnalyticsConfigurationProperties(); + } + + @Validated + @NoArgsConstructor + @Data + private static class AgmaAnalyticsConfigurationProperties { + + @NotNull + private AgmaAnalyticsHttpEndpointProperties endpoint; + + @NotNull + private AgmaAnalyticsBufferProperties buffers; + + @NotEmpty(message = "Please configure at least one account for Agma Analytics") + private List accounts; + + public AgmaAnalyticsProperties toComponentProperties() { + final Map accountsByPublisherId = accounts.stream() + .collect(Collectors.toMap( + this::buildPublisherSiteAppIdKey, + AgmaAnalyticsAccountProperties::getCode + )); + + return AgmaAnalyticsProperties.builder() + .url(endpoint.getUrl()) + .gzip(BooleanUtils.isTrue(endpoint.getGzip())) + .bufferSize(buffers.getSizeBytes()) + .maxEventsCount(buffers.getCount()) + .bufferTimeoutMs(buffers.getTimeoutMs()) + .httpTimeoutMs(endpoint.getTimeoutMs()) + .accounts(accountsByPublisherId) + .build(); + } + + private String buildPublisherSiteAppIdKey(AgmaAnalyticsAccountProperties account) { + final String publisherId = account.getPublisherId(); + final String siteAppId = account.getSiteAppId(); + return StringUtils.isNotBlank(siteAppId) + ? String.format("%s_%s", publisherId, siteAppId) + : publisherId; + } + + @Validated + @NoArgsConstructor + @Data + private static class AgmaAnalyticsHttpEndpointProperties { + + @NotNull + private String url; + + @NotNull + private Long timeoutMs; + + private Boolean gzip; + } + + @NoArgsConstructor + @Data + private static class AgmaAnalyticsBufferProperties { + + @NotNull + private Integer sizeBytes; + + @NotNull + private Integer count; + + @NotNull + private Long timeoutMs; + } + + @NoArgsConstructor + @Data + private static class AgmaAnalyticsAccountProperties { + + private String code; + + @NotNull + private String publisherId; + + private String siteAppId; + } + } + } + @Configuration @ConditionalOnProperty(prefix = "analytics.greenbids", name = "enabled", havingValue = "true") public static class GreenbidsAnalyticsConfiguration { diff --git a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java index 7edc69d6176..bfb56b8c0c1 100644 --- a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java @@ -1,16 +1,12 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpClientOptions; import lombok.Data; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; -import org.prebid.server.execution.RemoteFileSyncer; -import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; -import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; -import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.execution.file.syncer.FileSyncer; import org.prebid.server.geolocation.CircuitBreakerSecuredGeoLocationService; import org.prebid.server.geolocation.ConfigurationGeoLocationService; import org.prebid.server.geolocation.CountryCodeMapper; @@ -18,9 +14,7 @@ import org.prebid.server.geolocation.MaxMindGeoLocationService; import org.prebid.server.metric.Metrics; import org.prebid.server.spring.config.model.CircuitBreakerProperties; -import org.prebid.server.spring.config.model.ExponentialBackoffProperties; -import org.prebid.server.spring.config.model.HttpClientProperties; -import org.prebid.server.spring.config.model.RemoteFileSyncerProperties; +import org.prebid.server.spring.config.model.FileSyncerProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -57,14 +51,14 @@ CircuitBreakerProperties maxMindCircuitBreakerProperties() { @Bean @ConfigurationProperties(prefix = "geolocation.maxmind.remote-file-syncer") - RemoteFileSyncerProperties maxMindRemoteFileSyncerProperties() { - return new RemoteFileSyncerProperties(); + FileSyncerProperties maxMindRemoteFileSyncerProperties() { + return new FileSyncerProperties(); } @Bean @ConditionalOnProperty(prefix = "geolocation.circuit-breaker", name = "enabled", havingValue = "false", matchIfMissing = true) - GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncerProperties, + GeoLocationService basicGeoLocationService(FileSyncerProperties fileSyncerProperties, Vertx vertx) { return createGeoLocationService(fileSyncerProperties, vertx); @@ -75,7 +69,7 @@ GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncer CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( Vertx vertx, Metrics metrics, - RemoteFileSyncerProperties fileSyncerProperties, + FileSyncerProperties fileSyncerProperties, @Qualifier("maxMindCircuitBreakerProperties") CircuitBreakerProperties circuitBreakerProperties, Clock clock) { @@ -85,49 +79,12 @@ CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( circuitBreakerProperties.getClosingIntervalMs(), clock); } - private GeoLocationService createGeoLocationService(RemoteFileSyncerProperties properties, Vertx vertx) { + private GeoLocationService createGeoLocationService(FileSyncerProperties properties, Vertx vertx) { final MaxMindGeoLocationService maxMindGeoLocationService = new MaxMindGeoLocationService(); - final HttpClientProperties httpClientProperties = properties.getHttpClient(); - final HttpClientOptions httpClientOptions = new HttpClientOptions() - .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) - .setMaxRedirects(httpClientProperties.getMaxRedirects()); - - final RemoteFileSyncer remoteFileSyncer = new RemoteFileSyncer( - maxMindGeoLocationService, - properties.getDownloadUrl(), - properties.getSaveFilepath(), - properties.getTmpFilepath(), - toRetryPolicy(properties), - properties.getTimeoutMs(), - properties.getUpdateIntervalMs(), - vertx.createHttpClient(httpClientOptions), - vertx); - - remoteFileSyncer.sync(); + final FileSyncer fileSyncer = FileUtil.fileSyncerFor(maxMindGeoLocationService, properties, vertx); + fileSyncer.sync(); return maxMindGeoLocationService; } - - // TODO: remove after transition period - private static RetryPolicy toRetryPolicy(RemoteFileSyncerProperties properties) { - final Long retryIntervalMs = properties.getRetryIntervalMs(); - final Integer retryCount = properties.getRetryCount(); - final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount); - final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount) - || !fixedRetryPolicyDefined; - - if (!fixedRetryPolicyValid) { - throw new IllegalArgumentException("fixed interval retry policy is invalid"); - } - - final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry(); - return fixedRetryPolicyDefined - ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount) - : ExponentialBackoffRetryPolicy.of( - exponentialBackoffProperties.getDelayMillis(), - exponentialBackoffProperties.getMaxDelayMillis(), - exponentialBackoffProperties.getFactor(), - exponentialBackoffProperties.getJitter()); - } } @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java index 93eaeb48030..836b45ca285 100644 --- a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java @@ -2,7 +2,7 @@ import io.vertx.core.Vertx; import io.vertx.sqlclient.Pool; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.ApplicationChecker; import org.prebid.server.health.DatabaseHealthChecker; diff --git a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java index bffc5ee32f0..5a05ccb8c8e 100644 --- a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java @@ -3,11 +3,13 @@ import io.vertx.core.Vertx; import lombok.Data; import lombok.NoArgsConstructor; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.HookCatalog; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.v1.Module; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.HooksAdminConfig; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,6 +17,8 @@ import java.time.Clock; import java.util.Collection; +import java.util.Collections; +import java.util.Optional; @Configuration public class HooksConfiguration { @@ -30,16 +34,22 @@ HookStageExecutor hookStageExecutor(HooksConfigurationProperties hooksConfigurat TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + @Value("${settings.modules.require-config-to-invoke:false}") + boolean isConfigToInvokeRequired) { return HookStageExecutor.create( hooksConfiguration.getHostExecutionPlan(), hooksConfiguration.getDefaultAccountExecutionPlan(), + Optional.ofNullable(hooksConfiguration.getAdmin()) + .map(HooksAdminConfig::getModuleExecution) + .orElseGet(Collections::emptyMap), hookCatalog, timeoutFactory, vertx, clock, - mapper); + mapper, + isConfigToInvokeRequired); } @Bean @@ -56,5 +66,7 @@ private static class HooksConfigurationProperties { String hostExecutionPlan; String defaultAccountExecutionPlan; + + HooksAdminConfig admin; } } diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java index 79a62ffbe87..8a483e92a4d 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -3,7 +3,7 @@ import io.vertx.core.Vertx; import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.BasicPriceFloorAdjuster; import org.prebid.server.floors.BasicPriceFloorEnforcer; import org.prebid.server.floors.BasicPriceFloorProcessor; diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java index d9ac686d861..c7cac1ecf64 100644 --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java @@ -164,7 +164,8 @@ TcfDefinerService tcfDefinerService( GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, - Metrics metrics) { + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double samplingRate) { final Set eeaCountries = new HashSet<>(Arrays.asList(eeaCountriesAsString.trim().split(","))); @@ -175,7 +176,8 @@ TcfDefinerService tcfDefinerService( geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + samplingRate); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 71e55bbc780..deaa768320c 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -13,11 +13,13 @@ import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.BidResponseCreator; import org.prebid.server.auction.BidResponsePostProcessor; +import org.prebid.server.auction.BidsAdjuster; import org.prebid.server.auction.DebugResolver; import org.prebid.server.auction.DsaEnforcer; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.FpdResolver; import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.ImpAdjuster; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.IpAddressHelper; @@ -52,11 +54,9 @@ import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.CookieSyncPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory; -import org.prebid.server.auction.privacy.enforcement.ActivityEnforcement; import org.prebid.server.auction.privacy.enforcement.CcpaEnforcement; -import org.prebid.server.auction.privacy.enforcement.CoppaEnforcement; +import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcement; import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcementService; -import org.prebid.server.auction.privacy.enforcement.TcfEnforcement; import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; @@ -64,6 +64,9 @@ import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; +import org.prebid.server.bidadjustments.BidAdjustmentsResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.BidderErrorNotifier; @@ -82,7 +85,7 @@ import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.events.EventsService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; @@ -105,12 +108,14 @@ import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.BidValidationEnforcement; +import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties; import org.prebid.server.spring.config.model.HttpClientProperties; import org.prebid.server.util.VersionInfo; import org.prebid.server.util.system.CpuLoadAverageStats; import org.prebid.server.validation.BidderParamValidator; +import org.prebid.server.validation.ImpValidator; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.ResponseBidValidator; import org.prebid.server.validation.VideoRequestValidator; @@ -157,6 +162,8 @@ CoreCacheService cacheService( @Value("${cache.path}") String path, @Value("${cache.query}") String query, @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, + @Value("${pbc.api.key:#{null}}") String apiKey, + @Value("${cache.api-key-secured:false}") boolean apiKeySecured, VastModifier vastModifier, EventsService eventsService, HttpClient httpClient, @@ -169,6 +176,8 @@ CoreCacheService cacheService( CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), CacheServiceUtil.getCachedAssetUrlTemplate(scheme, host, path, query), expectedCacheTimeMs, + apiKey, + apiKeySecured, vastModifier, eventsService, metrics, @@ -245,6 +254,11 @@ FpdResolver fpdResolver(JacksonMapper mapper, JsonMerger jsonMerger) { return new FpdResolver(mapper, jsonMerger); } + @Bean + ImpAdjuster impAdjuster(ImpValidator impValidator, JacksonMapper jacksonMapper, JsonMerger jsonMerger) { + return new ImpAdjuster(jacksonMapper, jsonMerger, impValidator); + } + @Bean OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { return new OrtbTypesResolver(logSamplingRate, jacksonMapper, jsonMerger); @@ -377,7 +391,6 @@ Ortb2RequestFactory openRtb2RequestFactory( IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, CountryCodeMapper countryCodeMapper, - PriceFloorProcessor priceFloorProcessor, Metrics metrics) { final List blocklistedAccounts = splitToList(blocklistedAccountsString); @@ -395,7 +408,6 @@ Ortb2RequestFactory openRtb2RequestFactory( applicationSettings, ipAddressHelper, hookStageExecutor, - priceFloorProcessor, countryCodeMapper, metrics); } @@ -414,7 +426,8 @@ AuctionRequestFactory auctionRequestFactory( AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, JacksonMapper mapper, - GeoLocationServiceWrapper geoLocationServiceWrapper) { + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsRetriever bidAdjustmentsRetriever) { return new AuctionRequestFactory( maxRequestSize, @@ -430,7 +443,8 @@ AuctionRequestFactory auctionRequestFactory( auctionPrivacyContextFactory, debugResolver, mapper, - geoLocationServiceWrapper); + geoLocationServiceWrapper, + bidAdjustmentsRetriever); } @Bean @@ -742,11 +756,13 @@ HttpBidderRequester httpBidderRequester( HttpBidderRequestEnricher requestEnricher, JacksonMapper mapper) { - return new HttpBidderRequester(httpClient, + return new HttpBidderRequester( + httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier, requestEnricher, - mapper); + mapper, + logSamplingRate); } @Bean @@ -779,6 +795,16 @@ BidderErrorNotifier bidderErrorNotifier( metrics); } + @Bean + CacheDefaultTtlProperties cacheDefaultTtlProperties( + @Value("${cache.default-ttl-seconds.banner:300}") Integer bannerTtl, + @Value("${cache.default-ttl-seconds.video:1500}") Integer videoTtl, + @Value("${cache.default-ttl-seconds.audio:1500}") Integer audioTtl, + @Value("${cache.default-ttl-seconds.native:300}") Integer nativeTtl) { + + return CacheDefaultTtlProperties.of(bannerTtl, videoTtl, audioTtl, nativeTtl); + } + @Bean BidResponseCreator bidResponseCreator( CoreCacheService coreCacheService, @@ -794,7 +820,8 @@ BidResponseCreator bidResponseCreator( Clock clock, JacksonMapper mapper, @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl, - @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl) { + @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl, + CacheDefaultTtlProperties cacheDefaultTtlProperties) { return new BidResponseCreator( coreCacheService, @@ -809,7 +836,8 @@ BidResponseCreator bidResponseCreator( truncateAttrChars, clock, mapper, - CacheTtl.of(bannerCacheTtl, videoCacheTtl)); + CacheTtl.of(bannerCacheTtl, videoCacheTtl), + cacheDefaultTtlProperties); } @Bean @@ -819,6 +847,7 @@ ExchangeService exchangeService( StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, CompositeMediaTypeProcessor mediaTypeProcessor, @@ -827,17 +856,13 @@ ExchangeService exchangeService( TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyConversionService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, PriceFloorProcessor priceFloorProcessor, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -850,6 +875,7 @@ ExchangeService exchangeService( storedResponseProcessor, privacyEnforcementService, fpdResolver, + impAdjuster, supplyChainResolver, debugResolver, mediaTypeProcessor, @@ -858,23 +884,28 @@ ExchangeService exchangeService( timeoutFactory, bidRequestOrtbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyConversionService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, priceFloorProcessor, - dsaEnforcer, - bidAdjustmentFactorResolver, + bidsAdjuster, metrics, clock, mapper, criteriaLogManager, enabledStrictAppSiteDoohValidation); } + @Bean + BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + BidAdjustmentsProcessor bidAdjustmentsProcessor) { + + return new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); + } + @Bean StoredRequestProcessor storedRequestProcessor( @Value("${auction.stored-requests-timeout-ms}") long defaultTimeoutMs, @@ -913,16 +944,8 @@ StoredResponseProcessor storedResponseProcessor(ApplicationSettings applicationS } @Bean - PrivacyEnforcementService privacyEnforcementService(CoppaEnforcement coppaEnforcement, - CcpaEnforcement ccpaEnforcement, - TcfEnforcement tcfEnforcement, - ActivityEnforcement activityEnforcement) { - - return new PrivacyEnforcementService( - coppaEnforcement, - ccpaEnforcement, - tcfEnforcement, - activityEnforcement); + PrivacyEnforcementService privacyEnforcementService(List enforcements) { + return new PrivacyEnforcementService(enforcements); } @Bean @@ -989,10 +1012,18 @@ VersionInfo versionInfo(JacksonMapper jacksonMapper) { return VersionInfo.create("git-revision.json", jacksonMapper); } + @Bean + ImpValidator impValidator(BidderParamValidator bidderParamValidator, + BidderCatalog bidderCatalog, + JacksonMapper mapper) { + + return new ImpValidator(bidderParamValidator, bidderCatalog, mapper); + } + @Bean RequestValidator requestValidator( BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, @Value("${logging.sampling-rate:0.01}") double logSamplingRate, @@ -1000,7 +1031,7 @@ RequestValidator requestValidator( return new RequestValidator( bidderCatalog, - bidderParamValidator, + impValidator, metrics, mapper, logSamplingRate, @@ -1143,6 +1174,29 @@ SkippedAuctionService skipAuctionService(StoredResponseProcessor storedResponseP return new SkippedAuctionService(storedResponseProcessor, bidResponseCreator); } + @Bean + BidAdjustmentsRetriever bidAdjustmentsRetriever(JacksonMapper mapper, JsonMerger jsonMerger) { + return new BidAdjustmentsRetriever(mapper, jsonMerger, logSamplingRate); + } + + @Bean + BidAdjustmentsResolver bidAdjustmentsResolver(CurrencyConversionService currencyService) { + return new BidAdjustmentsResolver(currencyService); + } + + @Bean + BidAdjustmentsProcessor bidAdjustmentsProcessor(CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidAdjustmentsResolver bidAdjustmentsResolver, + JacksonMapper mapper) { + + return new BidAdjustmentsProcessor( + currencyService, + bidAdjustmentFactorResolver, + bidAdjustmentsResolver, + mapper); + } + private static List splitToList(String listAsString) { return splitToCollection(listAsString, ArrayList::new); } diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 1006403c9c4..f101495eb66 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -7,7 +7,7 @@ import lombok.experimental.UtilityClass; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.activity.ActivitiesConfigResolver; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; @@ -20,10 +20,12 @@ import org.prebid.server.settings.EnrichingApplicationSettings; import org.prebid.server.settings.FileApplicationSettings; import org.prebid.server.settings.HttpApplicationSettings; +import org.prebid.server.settings.S3ApplicationSettings; import org.prebid.server.settings.SettingsCache; import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.service.DatabasePeriodicRefreshService; import org.prebid.server.settings.service.HttpPeriodicRefreshService; +import org.prebid.server.settings.service.S3PeriodicRefreshService; import org.prebid.server.spring.config.database.DatabaseConfiguration; import org.prebid.server.vertx.database.DatabaseClient; import org.prebid.server.vertx.httpclient.HttpClient; @@ -37,12 +39,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Clock; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; @UtilityClass @@ -217,6 +227,115 @@ public DatabasePeriodicRefreshService ampDatabasePeriodicRefreshService( } } + @Configuration + @ConditionalOnProperty(prefix = "settings.s3", name = {"accounts-dir", "stored-imps-dir", "stored-requests-dir"}) + static class S3SettingsConfiguration { + + @Component + @ConfigurationProperties(prefix = "settings.s3") + @ConditionalOnProperty(prefix = "settings.s3", name = {"accessKeyId", "secretAccessKey"}) + @Validated + @Data + @NoArgsConstructor + protected static class S3ConfigurationProperties { + + @NotBlank + private String accessKeyId; + + @NotBlank + private String secretAccessKey; + + /** + * If not provided AWS_GLOBAL will be used as a region + */ + private String region; + + @NotBlank + private String endpoint; + + @NotBlank + private String bucket; + + @NotBlank + private Boolean forcePathStyle; + + @NotBlank + private String accountsDir; + + @NotBlank + private String storedImpsDir; + + @NotBlank + private String storedRequestsDir; + + @NotBlank + private String storedResponsesDir; + } + + @Bean + S3AsyncClient s3AsyncClient(S3ConfigurationProperties s3ConfigurationProperties) throws URISyntaxException { + final AwsBasicCredentials credentials = AwsBasicCredentials.create( + s3ConfigurationProperties.getAccessKeyId(), + s3ConfigurationProperties.getSecretAccessKey()); + final Region awsRegion = Optional.ofNullable(s3ConfigurationProperties.getRegion()) + .map(Region::of) + .orElse(Region.AWS_GLOBAL); + + return S3AsyncClient + .builder() + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .endpointOverride(new URI(s3ConfigurationProperties.getEndpoint())) + .forcePathStyle(s3ConfigurationProperties.getForcePathStyle()) + .region(awsRegion) + .build(); + } + + @Bean + S3ApplicationSettings s3ApplicationSettings(S3AsyncClient s3AsyncClient, + S3ConfigurationProperties s3ConfigurationProperties, + JacksonMapper mapper, + Vertx vertx) { + + return new S3ApplicationSettings( + s3AsyncClient, + s3ConfigurationProperties.getBucket(), + s3ConfigurationProperties.getAccountsDir(), + s3ConfigurationProperties.getStoredImpsDir(), + s3ConfigurationProperties.getStoredRequestsDir(), + s3ConfigurationProperties.getStoredResponsesDir(), + mapper, + vertx); + } + } + + @Configuration + @ConditionalOnProperty(prefix = "settings.in-memory-cache.s3-update", name = {"refresh-rate", "timeout"}) + static class S3PeriodicRefreshServiceConfiguration { + + @Bean + public S3PeriodicRefreshService s3PeriodicRefreshService( + S3AsyncClient s3AsyncClient, + S3SettingsConfiguration.S3ConfigurationProperties s3ConfigurationProperties, + @Value("${settings.in-memory-cache.s3-update.refresh-rate}") long refreshPeriod, + SettingsCache settingsCache, + Clock clock, + Metrics metrics, + Vertx vertx) { + + return new S3PeriodicRefreshService( + s3AsyncClient, + s3ConfigurationProperties.getBucket(), + s3ConfigurationProperties.getStoredRequestsDir(), + s3ConfigurationProperties.getStoredImpsDir(), + refreshPeriod, + settingsCache, + MetricName.stored_request, + clock, + metrics, + vertx); + } + } + /** * This configuration defines a collection of application settings fetchers and its ordering. */ @@ -227,14 +346,16 @@ static class CompositeSettingsConfiguration { CompositeApplicationSettings compositeApplicationSettings( @Autowired(required = false) FileApplicationSettings fileApplicationSettings, @Autowired(required = false) DatabaseApplicationSettings databaseApplicationSettings, - @Autowired(required = false) HttpApplicationSettings httpApplicationSettings) { + @Autowired(required = false) HttpApplicationSettings httpApplicationSettings, + @Autowired(required = false) S3ApplicationSettings s3ApplicationSettings) { - final List applicationSettingsList = - Stream.of(fileApplicationSettings, - databaseApplicationSettings, - httpApplicationSettings) - .filter(Objects::nonNull) - .toList(); + final List applicationSettingsList = Stream.of( + fileApplicationSettings, + databaseApplicationSettings, + s3ApplicationSettings, + httpApplicationSettings) + .filter(Objects::nonNull) + .toList(); return new CompositeApplicationSettings(applicationSettingsList); } @@ -338,7 +459,7 @@ SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperti @Validated @Data @NoArgsConstructor - private static class ApplicationSettingsCacheProperties { + protected static class ApplicationSettingsCacheProperties { @NotNull @Min(1) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java similarity index 59% rename from src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java rename to src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java index c65702aa9fa..8a86c88ac81 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.bizzclick.BizzclickBidder; +import org.prebid.server.bidder.adtonos.AdtonosBidder; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -16,26 +16,26 @@ import jakarta.validation.constraints.NotBlank; @Configuration -@PropertySource(value = "classpath:/bidder-config/bizzclick.yaml", factory = YamlPropertySourceFactory.class) -public class BizzclickConfiguration { +@PropertySource(value = "classpath:/bidder-config/adtonos.yaml", factory = YamlPropertySourceFactory.class) +public class AdtonosConfiguration { - private static final String BIDDER_NAME = "bizzclick"; + private static final String BIDDER_NAME = "adtonos"; - @Bean("bizzclickConfigurationProperties") - @ConfigurationProperties("adapters.bizzclick") + @Bean("adtonosConfigurationProperties") + @ConfigurationProperties("adapters.adtonos") BidderConfigurationProperties configurationProperties() { return new BidderConfigurationProperties(); } @Bean - BidderDeps bizzclickBidderDeps(BidderConfigurationProperties bizzclickConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + BidderDeps adtonosBidderDeps(BidderConfigurationProperties adtonosConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(bizzclickConfigurationProperties) + .withConfig(adtonosConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new BizzclickBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new AdtonosBidder(config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java new file mode 100644 index 00000000000..c5622397e71 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bidmatic.BidmaticBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/bidmatic.yaml", factory = YamlPropertySourceFactory.class) +public class BidmaticConfiguration { + + private static final String BIDDER_NAME = "bidmatic"; + + @Bean("bidmaticConfigurationProperties") + @ConfigurationProperties("adapters.bidmatic") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bidmaticBidderDeps(BidderConfigurationProperties bidmaticConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bidmaticConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BidmaticBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java new file mode 100644 index 00000000000..1c57db91aba --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.blasto.BlastoBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/blasto.yaml", factory = YamlPropertySourceFactory.class) +public class BlastoConfiguration { + + private static final String BIDDER_NAME = "blasto"; + + @Bean("blastoConfigurationProperties") + @ConfigurationProperties("adapters.blasto") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps blastoBidderDeps(BidderConfigurationProperties blastoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(blastoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BlastoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java new file mode 100644 index 00000000000..61340987965 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.copper6ssp.Copper6SspBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/copper6ssp.yaml", factory = YamlPropertySourceFactory.class) +public class Copper6SspConfiguration { + + private static final String BIDDER_NAME = "copper6ssp"; + + @Bean("copper6sspConfigurationProperties") + @ConfigurationProperties("adapters.copper6ssp") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps copper6sspBidderDeps(BidderConfigurationProperties copper6sspConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(copper6sspConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new Copper6SspBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java new file mode 100644 index 00000000000..29bd855b91b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.escalax.EscalaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/escalax.yaml", factory = YamlPropertySourceFactory.class) +public class EscalaxConfiguration { + + private static final String BIDDER_NAME = "escalax"; + + @Bean("escalaxConfigurationProperties") + @ConfigurationProperties("adapters.escalax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps escalaxBidderDeps(BidderConfigurationProperties escalaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(escalaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new EscalaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java new file mode 100644 index 00000000000..757f8059ed7 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java @@ -0,0 +1,40 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.loopme.LoopmeBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/loopme.yaml", factory = YamlPropertySourceFactory.class) +public class LoopmeConfiguration { + + private static final String BIDDER_NAME = "loopme"; + + @Bean("loopmeConfigurationProperties") + @ConfigurationProperties("adapters.loopme") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps loopmeBidderDeps(BidderConfigurationProperties loopmeConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(loopmeConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new LoopmeBidder(loopmeConfigurationProperties.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java similarity index 71% rename from src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java rename to src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java index 0111858f8e5..797ec11730c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.liftoff.LiftoffBidder; +import org.prebid.server.bidder.melozen.MeloZenBidder; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; @@ -17,27 +17,27 @@ import jakarta.validation.constraints.NotBlank; @Configuration -@PropertySource(value = "classpath:/bidder-config/liftoff.yaml", factory = YamlPropertySourceFactory.class) -public class LiftoffConfiguration { +@PropertySource(value = "classpath:/bidder-config/melozen.yaml", factory = YamlPropertySourceFactory.class) +public class MeloZenConfiguration { - private static final String BIDDER_NAME = "liftoff"; + private static final String BIDDER_NAME = "melozen"; - @Bean("liftoffConfigurationProperties") - @ConfigurationProperties("adapters.liftoff") + @Bean("melozenConfigurationProperties") + @ConfigurationProperties("adapters.melozen") BidderConfigurationProperties configurationProperties() { return new BidderConfigurationProperties(); } @Bean - BidderDeps liftoffBidderDeps(BidderConfigurationProperties liftoffConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, + BidderDeps melozenBidderDeps(BidderConfigurationProperties melozenConfigurationProperties, CurrencyConversionService currencyConversionService, + @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(liftoffConfigurationProperties) + .withConfig(melozenConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new LiftoffBidder(config.getEndpoint(), currencyConversionService, mapper)) + .bidderCreator(config -> new MeloZenBidder(currencyConversionService, config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java new file mode 100644 index 00000000000..df258ba30cd --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.metax.MetaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/metax.yaml", factory = YamlPropertySourceFactory.class) +public class MetaxConfiguration { + + private static final String BIDDER_NAME = "metax"; + + @Bean("metaxConfigurationProperties") + @ConfigurationProperties("adapters.metax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps metaxBidderDeps(BidderConfigurationProperties metaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(metaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MetaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java new file mode 100644 index 00000000000..1c9c7fd355f --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.missena.MissenaBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/missena.yaml", factory = YamlPropertySourceFactory.class) +public class MissenaConfiguration { + + private static final String BIDDER_NAME = "missena"; + + @Bean("missenaConfigurationProperties") + @ConfigurationProperties("adapters.missena") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps missenaBidderDeps(BidderConfigurationProperties missenaConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(missenaConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MissenaBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java new file mode 100644 index 00000000000..8c5fbc6abe2 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.oraki.OrakiBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/oraki.yaml", factory = YamlPropertySourceFactory.class) +public class OrakiConfiguration { + + private static final String BIDDER_NAME = "oraki"; + + @Bean("orakiConfigurationProperties") + @ConfigurationProperties("adapters.oraki") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps orakiBidderDeps(BidderConfigurationProperties orakiConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(orakiConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OrakiBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java new file mode 100644 index 00000000000..b34028b4221 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.ownadx.OwnAdxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/ownadx.yaml", factory = YamlPropertySourceFactory.class) +public class OwnAdxBidderConfiguration { + + private static final String BIDDER_NAME = "ownadx"; + + @Bean("ownAdxConfigurationProperties") + @ConfigurationProperties("adapters.ownadx") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps ownAdxBidderDeps(BidderConfigurationProperties ownAdxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(ownAdxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OwnAdxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java index 1e449c950d6..7296f81625b 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.pgamssp.PgamSspBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps pgamsspBidderDeps(BidderConfigurationProperties pgamsspConfigurationProperties, + CurrencyConversionService currencyConversionService, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(pgamsspConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), currencyConversionService, mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java new file mode 100644 index 00000000000..e8fd5755a58 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.pubrise.PubriseBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/pubrise.yaml", factory = YamlPropertySourceFactory.class) +public class PubriseConfiguration { + + private static final String BIDDER_NAME = "pubrise"; + + @Bean("pubriseConfigurationProperties") + @ConfigurationProperties("adapters.pubrise") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps pubriseBidderDeps(BidderConfigurationProperties pubriseConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(pubriseConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new PubriseBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java new file mode 100644 index 00000000000..0ccfe750403 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.qt.QtBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/qt.yaml", factory = YamlPropertySourceFactory.class) +public class QtConfiguration { + + private static final String BIDDER_NAME = "qt"; + + @Bean("qtConfigurationProperties") + @ConfigurationProperties("adapters.qt") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps qtBidderDeps(BidderConfigurationProperties qtConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(qtConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new QtBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java index 9580f229f1f..89f80e439aa 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java @@ -12,6 +12,7 @@ import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.version.PrebidVersionProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -40,6 +41,7 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration @NotBlank @Value("${external-url}") String externalUrl, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) @@ -49,13 +51,14 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration new RubiconBidder( BIDDER_NAME, config.getEndpoint(), + externalUrl, config.getXapi().getUsername(), config.getXapi().getPassword(), config.getMetaInfo().getSupportedVendors(), config.getGenerateBidId(), - config.getUseVideoSizeIdLogic(), currencyConversionService, floorResolver, + versionProvider, mapper)) .assemble(); } @@ -72,9 +75,6 @@ private static class RubiconConfigurationProperties extends BidderConfigurationP @NotNull private Boolean generateBidId; - - @NotNull - private Boolean useVideoSizeIdLogic; } @Data diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java index eaab8a70ef5..f640fe9f34d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.sonobi.SonobiBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps sonobiBidderDeps(BidderConfigurationProperties sonobiConfigurationProperties, + CurrencyConversionService currencyConversionService, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(sonobiConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new SonobiBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new SonobiBidder(currencyConversionService, config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java new file mode 100644 index 00000000000..899c29d2293 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java @@ -0,0 +1,62 @@ +package org.prebid.server.spring.config.bidder; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.thetradedesk.TheTradeDeskBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/thetradedesk.yaml", factory = YamlPropertySourceFactory.class) +public class TheTradeDeskConfiguration { + + private static final String BIDDER_NAME = "thetradedesk"; + + @Bean("thetradedeskConfigurationProperties") + @ConfigurationProperties("adapters.thetradedesk") + TheTradeDeskConfigurationProperties configurationProperties() { + return new TheTradeDeskConfigurationProperties(); + } + + @Bean + BidderDeps theTradeDeskBidderDeps(TheTradeDeskConfigurationProperties theTradeDeskConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(theTradeDeskConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TheTradeDeskBidder( + config.getEndpoint(), + mapper, + config.getExtraInfo().getSupplyId()) + ).assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class TheTradeDeskConfigurationProperties extends BidderConfigurationProperties { + + private ExtraInfo extraInfo = new ExtraInfo(); + } + + @Data + @NoArgsConstructor + private static class ExtraInfo { + + String supplyId; + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java new file mode 100644 index 00000000000..8bd04ffd8f3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.tradplus.TradPlusBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/tradplus.yaml", factory = YamlPropertySourceFactory.class) +public class TradPlusBidderConfiguration { + + private static final String BIDDER_NAME = "tradplus"; + + @Bean("tradplusConfigurationProperties") + @ConfigurationProperties("adapters.tradplus") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps tradplusBidderDeps(BidderConfigurationProperties tradplusConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(tradplusConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TradPlusBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java new file mode 100644 index 00000000000..a2bc6bd427e --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.vungle.VungleBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/vungle.yaml", factory = YamlPropertySourceFactory.class) +public class VungleConfiguration { + + private static final String BIDDER_NAME = "vungle"; + + @Bean("vungleConfigurationProperties") + @ConfigurationProperties("adapters.vungle") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps vungleBidderDeps(BidderConfigurationProperties vungleConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(vungleConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new VungleBidder(config.getEndpoint(), currencyConversionService, mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java index c25236dd799..bffc274c35d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java @@ -24,6 +24,8 @@ public class MetaInfo { private List supportedVendors; + private List currencyAccepted; + @NotNull private Integer vendorId; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java index 342ce998592..cd7553bb34a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java @@ -28,6 +28,7 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope metaInfo.getDoohMediaTypes(), metaInfo.getSupportedVendors(), metaInfo.getVendorId(), + metaInfo.getCurrencyAccepted(), configurationProperties.getPbsEnforcesCcpa(), configurationProperties.getModifyingVastXmlAllowed(), configurationProperties.getEndpointCompression(), diff --git a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java index 21d145bf826..daba3fda594 100644 --- a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java @@ -1,5 +1,6 @@ package org.prebid.server.spring.config.metrics; +import org.prebid.server.auction.HooksMetricsService; import org.slf4j.LoggerFactory; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.ConsoleReporter; @@ -134,6 +135,11 @@ AccountMetricsVerbosityResolver accountMetricsVerbosity(AccountsProperties accou accountsProperties.getDetailedVerbosity()); } + @Bean + HooksMetricsService hooksMetricsService(Metrics metrics) { + return new HooksMetricsService(metrics); + } + @Component @ConfigurationProperties(prefix = "metrics.graphite") @ConditionalOnProperty(prefix = "metrics.graphite", name = "enabled", havingValue = "true") diff --git a/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java new file mode 100644 index 00000000000..2a3e36b6ef1 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.spring.config.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class CacheDefaultTtlProperties { + + Integer bannerTtl; + + Integer videoTtl; + + Integer audioTtl; + + Integer nativeTtl; +} diff --git a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java similarity index 83% rename from src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java rename to src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java index 09e56ac59c6..54dbd81a5a9 100644 --- a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java @@ -11,7 +11,9 @@ @Validated @Data @NoArgsConstructor -public class RemoteFileSyncerProperties { +public class FileSyncerProperties { + + private Type type = Type.REMOTE; @NotBlank private String downloadUrl; @@ -37,6 +39,13 @@ public class RemoteFileSyncerProperties { @NotNull private Long updateIntervalMs; + private boolean checkSize; + @NotNull private HttpClientProperties httpClient; + + public enum Type { + + LOCAL, REMOTE + } } diff --git a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java index 82794d58ffb..b7c9eb405da 100644 --- a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java @@ -15,6 +15,7 @@ import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.gpp.CookieSyncGppService; @@ -29,7 +30,7 @@ import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.cookie.CookieSyncService; import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.handler.BidderParamHandler; import org.prebid.server.handler.CookieSyncHandler; import org.prebid.server.handler.ExceptionHandler; @@ -49,6 +50,7 @@ import org.prebid.server.handler.openrtb2.VideoHandler; import org.prebid.server.health.HealthChecker; import org.prebid.server.health.PeriodicHealthChecker; +import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.Metrics; @@ -210,9 +212,11 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler( AuctionRequestFactory auctionRequestFactory, AnalyticsReporterDelegator analyticsReporter, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { return new org.prebid.server.handler.openrtb2.AuctionHandler( @@ -222,9 +226,11 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler( skippedAuctionService, analyticsReporter, metrics, + hooksMetricsService, clock, httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, mapper); } @@ -234,12 +240,14 @@ AmpHandler openrtbAmpHandler( ExchangeService exchangeService, AnalyticsReporterDelegator analyticsReporter, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, BidderCatalog bidderCatalog, AmpProperties ampProperties, AmpResponsePostProcessor ampResponsePostProcessor, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { return new AmpHandler( @@ -247,12 +255,14 @@ AmpHandler openrtbAmpHandler( exchangeService, analyticsReporter, metrics, + hooksMetricsService, clock, bidderCatalog, ampProperties.getCustomTargetingSet(), ampResponsePostProcessor, httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, mapper, logSamplingRate); } @@ -265,8 +275,10 @@ VideoHandler openrtbVideoHandler( CoreCacheService coreCacheService, AnalyticsReporterDelegator analyticsReporter, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { return new VideoHandler( @@ -275,8 +287,10 @@ VideoHandler openrtbVideoHandler( exchangeService, coreCacheService, analyticsReporter, metrics, + hooksMetricsService, clock, prebidVersionProvider, + hookStageExecutor, mapper); } diff --git a/src/main/java/org/prebid/server/util/ListUtil.java b/src/main/java/org/prebid/server/util/ListUtil.java index e31aaa453ad..66efeaa1858 100644 --- a/src/main/java/org/prebid/server/util/ListUtil.java +++ b/src/main/java/org/prebid/server/util/ListUtil.java @@ -1,5 +1,6 @@ package org.prebid.server.util; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.util.algorithms.ListsUnionView; import java.util.List; @@ -12,4 +13,8 @@ private ListUtil() { public static List union(List first, List second) { return new ListsUnionView<>(first, second); } + + public static List nullIfEmpty(List value) { + return CollectionUtils.isEmpty(value) ? null : value; + } } diff --git a/src/main/java/org/prebid/server/util/PbsUtil.java b/src/main/java/org/prebid/server/util/PbsUtil.java new file mode 100644 index 00000000000..bc81f1ed2b5 --- /dev/null +++ b/src/main/java/org/prebid/server/util/PbsUtil.java @@ -0,0 +1,16 @@ +package org.prebid.server.util; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +public class PbsUtil { + + private PbsUtil() { + } + + public static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + return requestExt != null ? requestExt.getPrebid() : null; + } +} diff --git a/src/main/java/org/prebid/server/validation/ImpValidator.java b/src/main/java/org/prebid/server/validation/ImpValidator.java new file mode 100644 index 00000000000..d5d9ecba2f1 --- /dev/null +++ b/src/main/java/org/prebid/server/validation/ImpValidator.java @@ -0,0 +1,656 @@ +package org.prebid.server.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Asset; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.DataObject; +import com.iab.openrtb.request.EventTracker; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.ImageObject; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Metric; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Request; +import com.iab.openrtb.request.TitleObject; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.request.VideoObject; +import com.iab.openrtb.request.ntv.ContextSubType; +import com.iab.openrtb.request.ntv.ContextType; +import com.iab.openrtb.request.ntv.DataAssetType; +import com.iab.openrtb.request.ntv.EventTrackingMethod; +import com.iab.openrtb.request.ntv.EventType; +import com.iab.openrtb.request.ntv.PlacementType; +import com.iab.openrtb.request.ntv.Protocol; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; +import org.prebid.server.util.StreamUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +public class ImpValidator { + + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500; + + private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; + private static final String IMP_EXT = "imp"; + + private final BidderParamValidator bidderParamValidator; + private final BidderCatalog bidderCatalog; + private final JacksonMapper mapper; + + public ImpValidator(BidderParamValidator bidderParamValidator, BidderCatalog bidderCatalog, JacksonMapper mapper) { + this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.mapper = Objects.requireNonNull(mapper); + } + + public void validateImps(List imps, + Map aliases, + List warnings) throws ValidationException { + + for (int i = 0; i < imps.size(); i++) { + final Imp imp = imps.get(i); + validateImp(imp, "request.imp[%d]".formatted(i)); + fillAndValidateNative(imp.getXNative(), i); + validateImpExt(imp.getExt(), aliases, i, warnings); + } + } + + public void validateImp(Imp imp) throws ValidationException { + validateImp(imp, "imp[id=%s]".formatted(imp.getId())); + } + + private void validateImp(Imp imp, String msgPrefix) throws ValidationException { + if (StringUtils.isBlank(imp.getId())) { + throw new ValidationException("%s missing required field: \"id\"", msgPrefix); + } + if (imp.getMetric() != null && !imp.getMetric().isEmpty()) { + validateMetrics(imp.getMetric(), msgPrefix); + } + if (imp.getBanner() == null && imp.getVideo() == null && imp.getAudio() == null && imp.getXNative() == null) { + throw new ValidationException( + "%s must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", + msgPrefix); + } + + final boolean isInterstitialImp = Objects.equals(imp.getInstl(), 1); + validateBanner(imp.getBanner(), isInterstitialImp, msgPrefix); + validateVideoMimes(imp.getVideo(), msgPrefix); + validateAudioMimes(imp.getAudio(), msgPrefix); + validatePmp(imp.getPmp(), msgPrefix); + } + + private void fillAndValidateNative(Native xNative, int impIndex) throws ValidationException { + if (xNative == null) { + return; + } + + final Request nativeRequest = parseNativeRequest(xNative.getRequest(), impIndex); + + validateNativeContextTypes(nativeRequest.getContext(), nativeRequest.getContextsubtype(), impIndex); + validateNativePlacementType(nativeRequest.getPlcmttype(), impIndex); + final List updatedAssets = validateAndGetUpdatedNativeAssets(nativeRequest.getAssets(), impIndex); + validateNativeEventTrackers(nativeRequest.getEventtrackers(), impIndex); + + // modifier was added to reduce memory consumption on updating bidRequest.imp[i].native.request object + xNative.setRequest(toEncodedRequest(nativeRequest, updatedAssets)); + } + + private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { + if (StringUtils.isBlank(rawStringNativeRequest)) { + throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); + } + try { + return mapper.mapper().readValue(rawStringNativeRequest, Request.class); + } catch (IOException e) { + throw new ValidationException("Error while parsing request.imp[%d].native.request: %s", + impIndex, + ExceptionUtils.getMessage(e)); + } + } + + private void validateNativeContextTypes(Integer context, Integer contextSubType, int index) + throws ValidationException { + + final int type = context != null ? context : 0; + if (type == 0) { + return; + } + + if (type < ContextType.CONTENT.getValue() + || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index); + } + + final int subType = contextSubType != null ? contextSubType : 0; + if (subType < 0) { + throw new ValidationException( + "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); + } + + if (subType == 0) { + return; + } + + if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) { + if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) { + if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) { + if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); + } + } + + private void validateNativePlacementType(Integer placementType, int index) throws ValidationException { + final int type = placementType != null ? placementType : 0; + if (type == 0) { + return; + } + + if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue() + && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type); + } + } + + private List validateAndGetUpdatedNativeAssets(List assets, int impIndex) throws ValidationException { + if (CollectionUtils.isEmpty(assets)) { + throw new ValidationException( + "request.imp[%d].native.request.assets must be an array containing at least one object", impIndex); + } + + final List updatedAssets = new ArrayList<>(); + for (int i = 0; i < assets.size(); i++) { + final Asset asset = assets.get(i); + validateNativeAsset(asset, impIndex, i); + + final Asset updatedAsset = asset.getId() != null ? asset : asset.toBuilder().id(i).build(); + final boolean hasAssetWithId = updatedAssets.stream() + .map(Asset::getId) + .anyMatch(id -> id.equals(updatedAsset.getId())); + + if (hasAssetWithId) { + throw new ValidationException("request.imp[%d].native.request.assets[%d].id is already being used by " + + "another asset. Each asset ID must be unique.", impIndex, i); + } + + updatedAssets.add(updatedAsset); + } + return updatedAssets; + } + + private void validateNativeAsset(Asset asset, int impIndex, int assetIndex) throws ValidationException { + final TitleObject title = asset.getTitle(); + final ImageObject image = asset.getImg(); + final VideoObject video = asset.getVideo(); + final DataObject data = asset.getData(); + + final long assetsCount = Stream.of(title, image, video, data) + .filter(Objects::nonNull) + .count(); + + if (assetsCount > 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", + impIndex, assetIndex); + } + + validateNativeAssetTitle(title, impIndex, assetIndex); + validateNativeAssetVideo(video, impIndex, assetIndex); + validateNativeAssetData(data, impIndex, assetIndex); + } + + private void validateNativeAssetTitle(TitleObject title, int impIndex, int assetIndex) throws ValidationException { + if (title != null && (title.getLen() == null || title.getLen() < 1)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].title.len must be a positive integer", + impIndex, assetIndex); + } + } + + private void validateNativeAssetData(DataObject data, int impIndex, int assetIndex) throws ValidationException { + if (data == null || data.getType() == null) { + return; + } + + final Integer type = data.getType(); + if (type < DataAssetType.SPONSORED.getValue() + || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: " + + documentationOnPage(40), impIndex, assetIndex); + } + } + + private void validateNativeAssetVideo(VideoObject video, int impIndex, int assetIndex) throws ValidationException { + if (video == null) { + return; + } + + if (CollectionUtils.isEmpty(video.getMimes())) { + throw new ValidationException("request.imp[%d].native.request.assets[%d].video.mimes must be an " + + "array with at least one MIME type", impIndex, assetIndex); + } + + if (video.getMinduration() == null || video.getMinduration() < 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", + impIndex, assetIndex); + } + + if (video.getMaxduration() == null || video.getMaxduration() < 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", + impIndex, assetIndex); + } + + validateNativeVideoProtocols(video.getProtocols(), impIndex, assetIndex); + } + + private void validateNativeVideoProtocols(List protocols, int impIndex, int assetIndex) + throws ValidationException { + if (CollectionUtils.isEmpty(protocols)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least" + + " one element", impIndex, assetIndex); + } + + for (int i = 0; i < protocols.size(); i++) { + validateNativeVideoProtocol(protocols.get(i), impIndex, assetIndex, i); + } + } + + private void validateNativeVideoProtocol(Integer protocol, int impIndex, int assetIndex, int protocolIndex) + throws ValidationException { + if (protocol < Protocol.VAST10.getValue() || protocol > Protocol.DAAST10_WRAPPER.getValue()) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]." + + " Got %d", impIndex, assetIndex, protocolIndex, protocol); + } + } + + private void validateNativeEventTrackers(List eventTrackers, int impIndex) + throws ValidationException { + + if (CollectionUtils.isNotEmpty(eventTrackers)) { + for (int eventTrackerIndex = 0; eventTrackerIndex < eventTrackers.size(); eventTrackerIndex++) { + validateNativeEventTracker(eventTrackers.get(eventTrackerIndex), impIndex, eventTrackerIndex); + } + } + } + + private void validateNativeEventTracker(EventTracker eventTracker, int impIndex, int eventIndex) + throws ValidationException { + if (eventTracker != null) { + final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0; + + if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue() + && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: " + + documentationOnPage(43), impIndex, eventIndex + ); + } + + final List methods = eventTracker.getMethods(); + + if (CollectionUtils.isEmpty(methods)) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: " + + documentationOnPage(43), impIndex, eventIndex + ); + } + + for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) { + final int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0; + if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue() + && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: " + + documentationOnPage(43), impIndex, eventIndex, methodIndex + ); + } + } + } + } + + private void validateImpExt(ObjectNode ext, Map aliases, int impIndex, + List warnings) throws ValidationException { + validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex, warnings); + } + + private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex, + List warnings) + throws ValidationException { + + if (extPrebidNode == null) { + throw new ValidationException( + "request.imp[%d].ext.prebid must be defined", impIndex); + } + + if (!extPrebidNode.isObject()) { + throw new ValidationException( + "request.imp[%d].ext.prebid must an object type", impIndex); + } + + final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT); + + if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder must be an object type", impIndex); + } + final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex); + + validateImpExtPrebidBidder(extPrebidBidderNode, extPrebid.getStoredAuctionResponse(), + aliases, impIndex, warnings); + validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex, warnings); + + validateImpExtPrebidImp(extPrebidNode.get(IMP_EXT), aliases, impIndex, warnings); + } + + private void validateImpExtPrebidImp(JsonNode imp, + Map aliases, + int impIndex, + List warnings) { + if (imp == null) { + return; + } + + final Iterator> bidders = imp.fields(); + while (bidders.hasNext()) { + final Map.Entry bidder = bidders.next(); + final String bidderName = bidder.getKey(); + final String resolvedBidderName = aliases.getOrDefault(bidderName, bidderName); + if (!bidderCatalog.isValidName(resolvedBidderName) && !bidderCatalog.isDeprecatedName(resolvedBidderName)) { + bidders.remove(); + warnings.add("WARNING: request.imp[%d].ext.prebid.imp.%s was dropped with the reason: invalid bidder" + .formatted(impIndex, bidderName)); + } + } + } + + private void validateImpExtPrebidBidder(JsonNode extPrebidBidder, + ExtStoredAuctionResponse storedAuctionResponse, + Map aliases, + int impIndex, + List warnings) throws ValidationException { + if (extPrebidBidder == null) { + if (storedAuctionResponse != null) { + return; + } else { + throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex); + } + } + + final Iterator> bidderExtensions = extPrebidBidder.fields(); + while (bidderExtensions.hasNext()) { + final Map.Entry bidderExtension = bidderExtensions.next(); + final String bidder = bidderExtension.getKey(); + try { + validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); + } catch (ValidationException ex) { + bidderExtensions.remove(); + warnings.add("WARNING: request.imp[%d].ext.prebid.bidder.%s was dropped with a reason: %s" + .formatted(impIndex, bidder, ex.getMessage())); + } + } + + if (extPrebidBidder.isEmpty()) { + warnings.add("WARNING: request.imp[%d].ext must contain at least one valid bidder".formatted(impIndex)); + } + } + + private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, + Map aliases, + int impIndex, + List warnings) throws ValidationException { + final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse(); + if (extStoredAuctionResponse != null) { + if (extStoredAuctionResponse.getSeatBids() != null) { + warnings.add("WARNING: request.imp[%d].ext.prebid.storedauctionresponse.seatbidarr".formatted(impIndex) + + " is not supported at the imp level"); + } + + if (extStoredAuctionResponse.getId() == null && extStoredAuctionResponse.getSeatBid() == null) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedauctionresponse.{id or seatbidobj} should be defined", + impIndex); + } + } + + final List storedBidResponses = extPrebid.getStoredBidResponse(); + if (CollectionUtils.isNotEmpty(storedBidResponses)) { + final ObjectNode bidderNode = extPrebid.getBidder(); + if (bidderNode == null || bidderNode.isEmpty()) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse" + .formatted(impIndex)); + } + + for (ExtStoredBidResponse storedBidResponse : storedBidResponses) { + validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex); + } + } + } + + private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode, + Map aliases, int impIndex) throws ValidationException { + final String bidder = extStoredBidResponse.getBidder(); + final String id = extStoredBidResponse.getId(); + if (StringUtils.isEmpty(bidder)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined".formatted(impIndex)); + } + + if (StringUtils.isEmpty(id)) { + throw new ValidationException( + "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id".formatted(impIndex)); + } + + final String resolvedBidder = aliases.getOrDefault(bidder, bidder); + + if (!bidderCatalog.isValidName(resolvedBidder)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder".formatted(impIndex)); + } + + final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames()) + .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder)); + if (noCorrespondentBidderParameters) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters" + .formatted(impIndex)); + } + } + + private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException { + try { + return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new ValidationException(" bidRequest.imp[%d].ext.prebid: %s has invalid format" + .formatted(impIndex, e.getMessage())); + } + } + + private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName) + throws ValidationException { + if (bidderCatalog.isValidName(bidderName)) { + final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); + if (!messages.isEmpty()) { + throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, + bidderName, String.join("\n", messages)); + } + } else if (!bidderCatalog.isDeprecatedName(bidderName)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); + } + } + + private void validatePmp(Pmp pmp, String msgPrefix) throws ValidationException { + if (pmp != null && pmp.getDeals() != null) { + for (int dealIndex = 0; dealIndex < pmp.getDeals().size(); dealIndex++) { + if (StringUtils.isBlank(pmp.getDeals().get(dealIndex).getId())) { + throw new ValidationException("%s.pmp.deals[%d] missing required field: \"id\"", + msgPrefix, dealIndex); + } + } + } + } + + private void validateBanner(Banner banner, boolean isInterstitial, String msgPrefix) throws ValidationException { + if (banner != null) { + final Integer width = banner.getW(); + final Integer height = banner.getH(); + final boolean hasWidth = hasPositiveValue(width); + final boolean hasHeight = hasPositiveValue(height); + final boolean hasSize = hasWidth && hasHeight; + + final List format = banner.getFormat(); + if (CollectionUtils.isEmpty(format) && !hasSize && !isInterstitial) { + throw new ValidationException("%s.banner has no sizes. Define \"w\" and \"h\", " + + "or include \"format\" elements", msgPrefix); + } + + if (width != null && height != null && !hasSize && !isInterstitial) { + throw new ValidationException("%s.banner must define a valid" + + " \"h\" and \"w\" properties", msgPrefix); + } + + if (format != null) { + for (int formatIndex = 0; formatIndex < format.size(); formatIndex++) { + validateFormat(format.get(formatIndex), msgPrefix, formatIndex); + } + } + } + } + + private void validateFormat(Format format, String msgPrefix, int formatIndex) throws ValidationException { + final boolean usesH = hasPositiveValue(format.getH()); + final boolean usesW = hasPositiveValue(format.getW()); + final boolean usesWmin = hasPositiveValue(format.getWmin()); + final boolean usesWratio = hasPositiveValue(format.getWratio()); + final boolean usesHratio = hasPositiveValue(format.getHratio()); + final boolean usesHW = usesH || usesW; + final boolean usesRatios = usesWmin || usesWratio || usesHratio; + + if (usesHW && usesRatios) { + throw new ValidationException("%s.banner.format[%d] should define *either*" + + " {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" " + + "objects in the request", msgPrefix, formatIndex); + } + + if (!usesHW && !usesRatios) { + throw new ValidationException("%s.banner.format[%d] should define *either*" + + " {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) " + + "to be non-zero positive", msgPrefix, formatIndex); + } + + if (usesHW && (!usesH || !usesW)) { + throw new ValidationException("%s.banner.format[%d] must define a valid" + + " \"h\" and \"w\" properties", msgPrefix, formatIndex); + } + + if (usesRatios && (!usesWmin || !usesWratio || !usesHratio)) { + throw new ValidationException("%s.banner.format[%d] must define a valid" + + " \"wmin\", \"wratio\", and \"hratio\" properties", msgPrefix, formatIndex); + } + } + + private void validateVideoMimes(Video video, String msgPrefix) throws ValidationException { + if (video != null) { + validateMimes(video.getMimes(), + "%s.video.mimes must contain at least one supported MIME type", msgPrefix); + } + } + + private void validateAudioMimes(Audio audio, String msgPrefix) throws ValidationException { + if (audio != null) { + validateMimes(audio.getMimes(), + "%s.audio.mimes must contain at least one supported MIME type", msgPrefix); + } + } + + private void validateMimes(List mimes, String msg, String msgPrefix) throws ValidationException { + if (CollectionUtils.isEmpty(mimes)) { + throw new ValidationException(msg, msgPrefix); + } + } + + private void validateMetrics(List metrics, String msgPrefix) throws ValidationException { + for (int i = 0; i < metrics.size(); i++) { + final Metric metric = metrics.get(i); + + if (StringUtils.isEmpty(metric.getType())) { + throw new ValidationException("Missing %s.metric[%d].type", msgPrefix, i); + } + + final Float value = metric.getValue(); + if (value == null || value < 0.0 || value > 1.0) { + throw new ValidationException("%s.metric[%d].value must be in the range [0.0, 1.0]", msgPrefix, i); + } + } + } + + private String toEncodedRequest(Request nativeRequest, List updatedAssets) { + try { + return mapper.mapper().writeValueAsString(nativeRequest.toBuilder().assets(updatedAssets).build()); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Error while marshaling native request to the string", e); + } + } + + private static String documentationOnPage(int page) { + return "%s#page=%d".formatted(DOCUMENTATION, page); + } + + private static boolean hasPositiveValue(Integer value) { + return value != null && value > 0; + } + +} diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index 6076aefebe9..f596850955d 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -3,41 +3,18 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Asset; -import com.iab.openrtb.request.Audio; -import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.DataObject; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.EventTracker; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.ImageObject; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Metric; -import com.iab.openrtb.request.Native; -import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Request; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.TitleObject; -import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; -import com.iab.openrtb.request.VideoObject; -import com.iab.openrtb.request.ntv.ContextSubType; -import com.iab.openrtb.request.ntv.ContextType; -import com.iab.openrtb.request.ntv.DataAssetType; -import com.iab.openrtb.request.ntv.EventTrackingMethod; -import com.iab.openrtb.request.ntv.EventType; -import com.iab.openrtb.request.ntv.PlacementType; -import com.iab.openrtb.request.ntv.Protocol; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; @@ -49,7 +26,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -60,30 +36,24 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.StreamUtil; import org.prebid.server.validation.model.ValidationResult; -import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; /** * A component that validates {@link BidRequest} objects for openrtb2 auction endpoint. @@ -94,17 +64,11 @@ public class RequestValidator { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( LoggerFactory.getLogger(RequestValidator.class)); - private static final String PREBID_EXT = "prebid"; - private static final String BIDDER_EXT = "bidder"; private static final String ASTERISK = "*"; private static final Locale LOCALE = Locale.US; - private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500; - - private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; private final BidderCatalog bidderCatalog; - private final BidderParamValidator bidderParamValidator; + private final ImpValidator impValidator; private final Metrics metrics; private final JacksonMapper mapper; private final double logSamplingRate; @@ -115,14 +79,13 @@ public class RequestValidator { * properties of bidRequest. */ public RequestValidator(BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, - Metrics metrics, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, double logSamplingRate, boolean enabledStrictAppSiteDoohValidation) { this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); + this.impValidator = Objects.requireNonNull(impValidator); this.metrics = Objects.requireNonNull(metrics); this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; @@ -186,9 +149,7 @@ public ValidationResult validate(BidRequest bidRequest, HttpRequestContext httpR throw new ValidationException(String.join(System.lineSeparator(), errors)); } - for (int index = 0; index < bidRequest.getImp().size(); index++) { - validateImp(bidRequest.getImp().get(index), aliases, index, warnings); - } + impValidator.validateImps(bidRequest.getImp(), aliases, warnings); final List channels = new ArrayList<>(); Optional.ofNullable(bidRequest.getApp()).ifPresent(ignored -> channels.add("request.app")); @@ -610,19 +571,6 @@ private void validateUser(User user, Map aliases) throws Validat throw new ValidationException( "request.user.eids[%d] missing required field: \"source\"", index); } - final List eidUids = eid.getUids(); - if (CollectionUtils.isEmpty(eidUids)) { - throw new ValidationException( - "request.user.eids[%d].uids must contain at least one element", index); - } - for (int uidsIndex = 0; uidsIndex < eidUids.size(); uidsIndex++) { - final Uid uid = eidUids.get(uidsIndex); - if (StringUtils.isBlank(uid.getId())) { - throw new ValidationException( - "request.user.eids[%d].uids[%d] missing required field: \"id\"", index, - uidsIndex); - } - } } } } @@ -660,557 +608,4 @@ private void validateRegs(Regs regs) throws ValidationException { } } - private void validateImp(Imp imp, Map aliases, int index, List warnings) - throws ValidationException { - if (StringUtils.isBlank(imp.getId())) { - throw new ValidationException("request.imp[%d] missing required field: \"id\"", index); - } - if (imp.getMetric() != null && !imp.getMetric().isEmpty()) { - validateMetrics(imp.getMetric(), index); - } - if (imp.getBanner() == null && imp.getVideo() == null && imp.getAudio() == null && imp.getXNative() == null) { - throw new ValidationException( - "request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", - index); - } - - final boolean isInterstitialImp = Objects.equals(imp.getInstl(), 1); - validateBanner(imp.getBanner(), isInterstitialImp, index); - validateVideoMimes(imp.getVideo(), index); - validateAudioMimes(imp.getAudio(), index); - fillAndValidateNative(imp.getXNative(), index); - validatePmp(imp.getPmp(), index); - validateImpExt(imp.getExt(), aliases, index, warnings); - } - - private void fillAndValidateNative(Native xNative, int impIndex) throws ValidationException { - if (xNative == null) { - return; - } - - final Request nativeRequest = parseNativeRequest(xNative.getRequest(), impIndex); - - validateNativeContextTypes(nativeRequest.getContext(), nativeRequest.getContextsubtype(), impIndex); - validateNativePlacementType(nativeRequest.getPlcmttype(), impIndex); - final List updatedAssets = validateAndGetUpdatedNativeAssets(nativeRequest.getAssets(), impIndex); - validateNativeEventTrackers(nativeRequest.getEventtrackers(), impIndex); - - // modifier was added to reduce memory consumption on updating bidRequest.imp[i].native.request object - xNative.setRequest(toEncodedRequest(nativeRequest, updatedAssets)); - } - - private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { - if (StringUtils.isBlank(rawStringNativeRequest)) { - throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); - } - try { - return mapper.mapper().readValue(rawStringNativeRequest, Request.class); - } catch (IOException e) { - throw new ValidationException("Error while parsing request.imp[%d].native.request: %s", - impIndex, - ExceptionUtils.getMessage(e)); - } - } - - private void validateNativeContextTypes(Integer context, Integer contextSubType, int index) - throws ValidationException { - - final int type = context != null ? context : 0; - if (type == 0) { - return; - } - - if (type < ContextType.CONTENT.getValue() - || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index); - } - - final int subType = contextSubType != null ? contextSubType : 0; - if (subType < 0) { - throw new ValidationException( - "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); - } - - if (subType == 0) { - return; - } - - if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) { - if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) { - if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) { - if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); - } - } - - private void validateNativePlacementType(Integer placementType, int index) throws ValidationException { - final int type = placementType != null ? placementType : 0; - if (type == 0) { - return; - } - - if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue() - && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type); - } - } - - private List validateAndGetUpdatedNativeAssets(List assets, int impIndex) throws ValidationException { - if (CollectionUtils.isEmpty(assets)) { - throw new ValidationException( - "request.imp[%d].native.request.assets must be an array containing at least one object", impIndex); - } - - final List updatedAssets = new ArrayList<>(); - for (int i = 0; i < assets.size(); i++) { - final Asset asset = assets.get(i); - validateNativeAsset(asset, impIndex, i); - - final Asset updatedAsset = asset.getId() != null ? asset : asset.toBuilder().id(i).build(); - final boolean hasAssetWithId = updatedAssets.stream() - .map(Asset::getId) - .anyMatch(id -> id.equals(updatedAsset.getId())); - - if (hasAssetWithId) { - throw new ValidationException("request.imp[%d].native.request.assets[%d].id is already being used by " - + "another asset. Each asset ID must be unique.", impIndex, i); - } - - updatedAssets.add(updatedAsset); - } - return updatedAssets; - } - - private void validateNativeAsset(Asset asset, int impIndex, int assetIndex) throws ValidationException { - final TitleObject title = asset.getTitle(); - final ImageObject image = asset.getImg(); - final VideoObject video = asset.getVideo(); - final DataObject data = asset.getData(); - - final long assetsCount = Stream.of(title, image, video, data) - .filter(Objects::nonNull) - .count(); - - if (assetsCount > 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", - impIndex, assetIndex); - } - - validateNativeAssetTitle(title, impIndex, assetIndex); - validateNativeAssetVideo(video, impIndex, assetIndex); - validateNativeAssetData(data, impIndex, assetIndex); - } - - private void validateNativeAssetTitle(TitleObject title, int impIndex, int assetIndex) throws ValidationException { - if (title != null && (title.getLen() == null || title.getLen() < 1)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].title.len must be a positive integer", - impIndex, assetIndex); - } - } - - private void validateNativeAssetData(DataObject data, int impIndex, int assetIndex) throws ValidationException { - if (data == null || data.getType() == null) { - return; - } - - final Integer type = data.getType(); - if (type < DataAssetType.SPONSORED.getValue() - || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: " - + documentationOnPage(40), impIndex, assetIndex); - } - } - - private void validateNativeAssetVideo(VideoObject video, int impIndex, int assetIndex) throws ValidationException { - if (video == null) { - return; - } - - if (CollectionUtils.isEmpty(video.getMimes())) { - throw new ValidationException("request.imp[%d].native.request.assets[%d].video.mimes must be an " - + "array with at least one MIME type", impIndex, assetIndex); - } - - if (video.getMinduration() == null || video.getMinduration() < 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", - impIndex, assetIndex); - } - - if (video.getMaxduration() == null || video.getMaxduration() < 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", - impIndex, assetIndex); - } - - validateNativeVideoProtocols(video.getProtocols(), impIndex, assetIndex); - } - - private void validateNativeVideoProtocols(List protocols, int impIndex, int assetIndex) - throws ValidationException { - if (CollectionUtils.isEmpty(protocols)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least" - + " one element", impIndex, assetIndex); - } - - for (int i = 0; i < protocols.size(); i++) { - validateNativeVideoProtocol(protocols.get(i), impIndex, assetIndex, i); - } - } - - private void validateNativeVideoProtocol(Integer protocol, int impIndex, int assetIndex, int protocolIndex) - throws ValidationException { - if (protocol < Protocol.VAST10.getValue() || protocol > Protocol.DAAST10_WRAPPER.getValue()) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]." - + " Got %d", impIndex, assetIndex, protocolIndex, protocol); - } - } - - private void validateNativeEventTrackers(List eventTrackers, int impIndex) - throws ValidationException { - - if (CollectionUtils.isNotEmpty(eventTrackers)) { - for (int eventTrackerIndex = 0; eventTrackerIndex < eventTrackers.size(); eventTrackerIndex++) { - validateNativeEventTracker(eventTrackers.get(eventTrackerIndex), impIndex, eventTrackerIndex); - } - } - } - - private void validateNativeEventTracker(EventTracker eventTracker, int impIndex, int eventIndex) - throws ValidationException { - if (eventTracker != null) { - final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0; - - if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue() - && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: " - + documentationOnPage(43), impIndex, eventIndex - ); - } - - final List methods = eventTracker.getMethods(); - - if (CollectionUtils.isEmpty(methods)) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: " - + documentationOnPage(43), impIndex, eventIndex - ); - } - - for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) { - final int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0; - if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue() - && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: " - + documentationOnPage(43), impIndex, eventIndex, methodIndex - ); - } - } - } - } - - private static String documentationOnPage(int page) { - return "%s#page=%d".formatted(DOCUMENTATION, page); - } - - private String toEncodedRequest(Request nativeRequest, List updatedAssets) { - try { - return mapper.mapper().writeValueAsString(nativeRequest.toBuilder().assets(updatedAssets).build()); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Error while marshaling native request to the string", e); - } - } - - private void validateImpExt(ObjectNode ext, Map aliases, int impIndex, - List warnings) throws ValidationException { - validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex, warnings); - } - - private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex, - List warnings) - throws ValidationException { - - if (extPrebidNode == null) { - throw new ValidationException( - "request.imp[%d].ext.prebid must be defined", impIndex); - } - - if (!extPrebidNode.isObject()) { - throw new ValidationException( - "request.imp[%d].ext.prebid must an object type", impIndex); - } - - final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT); - - if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder must be an object type", impIndex); - } - final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex); - - validateImpExtPrebidBidder(extPrebidBidderNode, extPrebid.getStoredAuctionResponse(), - aliases, impIndex, warnings); - validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex, warnings); - } - - private void validateImpExtPrebidBidder(JsonNode extPrebidBidder, - ExtStoredAuctionResponse storedAuctionResponse, - Map aliases, - int impIndex, - List warnings) throws ValidationException { - if (extPrebidBidder == null) { - if (storedAuctionResponse != null) { - return; - } else { - throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex); - } - } - - final Iterator> bidderExtensions = extPrebidBidder.fields(); - while (bidderExtensions.hasNext()) { - final Map.Entry bidderExtension = bidderExtensions.next(); - final String bidder = bidderExtension.getKey(); - try { - validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); - } catch (ValidationException ex) { - bidderExtensions.remove(); - warnings.add("WARNING: request.imp[%d].ext.prebid.bidder.%s was dropped with a reason: %s" - .formatted(impIndex, bidder, ex.getMessage())); - } - } - - if (extPrebidBidder.isEmpty()) { - warnings.add("WARNING: request.imp[%d].ext must contain at least one valid bidder".formatted(impIndex)); - } - } - - private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, - Map aliases, - int impIndex, - List warnings) throws ValidationException { - - final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse(); - if (extStoredAuctionResponse != null) { - if (extStoredAuctionResponse.getSeatBids() != null) { - warnings.add("WARNING: request.imp[%d].ext.prebid.storedauctionresponse.seatbidarr".formatted(impIndex) - + " is not supported at the imp level"); - } - - if (extStoredAuctionResponse.getId() == null) { - throw new ValidationException("request.imp[%d].ext.prebid.storedauctionresponse.id should be defined", - impIndex); - } - } - - final List storedBidResponses = extPrebid.getStoredBidResponse(); - if (CollectionUtils.isNotEmpty(storedBidResponses)) { - final ObjectNode bidderNode = extPrebid.getBidder(); - if (bidderNode == null || bidderNode.isEmpty()) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse" - .formatted(impIndex)); - } - - for (ExtStoredBidResponse storedBidResponse : storedBidResponses) { - validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex); - } - } - } - - private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode, - Map aliases, int impIndex) throws ValidationException { - final String bidder = extStoredBidResponse.getBidder(); - final String id = extStoredBidResponse.getId(); - if (StringUtils.isEmpty(bidder)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined".formatted(impIndex)); - } - - if (StringUtils.isEmpty(id)) { - throw new ValidationException( - "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id".formatted(impIndex)); - } - - final String resolvedBidder = aliases.getOrDefault(bidder, bidder); - - if (!bidderCatalog.isValidName(resolvedBidder)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder".formatted(impIndex)); - } - - final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames()) - .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder)); - if (noCorrespondentBidderParameters) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters" - .formatted(impIndex)); - } - } - - private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new ValidationException(" bidRequest.imp[%d].ext.prebid: %s has invalid format" - .formatted(impIndex, e.getMessage())); - } - } - - private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName) - throws ValidationException { - if (bidderCatalog.isValidName(bidderName)) { - final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); - if (!messages.isEmpty()) { - throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, - bidderName, String.join("\n", messages)); - } - } else if (!bidderCatalog.isDeprecatedName(bidderName)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); - } - } - - private void validatePmp(Pmp pmp, int impIndex) throws ValidationException { - if (pmp != null && pmp.getDeals() != null) { - for (int dealIndex = 0; dealIndex < pmp.getDeals().size(); dealIndex++) { - if (StringUtils.isBlank(pmp.getDeals().get(dealIndex).getId())) { - throw new ValidationException("request.imp[%d].pmp.deals[%d] missing required field: \"id\"", - impIndex, dealIndex); - } - } - } - } - - private void validateBanner(Banner banner, boolean isInterstitial, int impIndex) throws ValidationException { - if (banner != null) { - final Integer width = banner.getW(); - final Integer height = banner.getH(); - final boolean hasWidth = hasPositiveValue(width); - final boolean hasHeight = hasPositiveValue(height); - final boolean hasSize = hasWidth && hasHeight; - - final List format = banner.getFormat(); - if (CollectionUtils.isEmpty(format) && !hasSize && !isInterstitial) { - throw new ValidationException("request.imp[%d].banner has no sizes. Define \"w\" and \"h\", " - + "or include \"format\" elements", impIndex); - } - - if (width != null && height != null && !hasSize && !isInterstitial) { - throw new ValidationException("Request imp[%d].banner must define a valid" - + " \"h\" and \"w\" properties", impIndex); - } - - if (format != null) { - for (int formatIndex = 0; formatIndex < format.size(); formatIndex++) { - validateFormat(format.get(formatIndex), impIndex, formatIndex); - } - } - } - } - - private void validateFormat(Format format, int impIndex, int formatIndex) throws ValidationException { - final boolean usesH = hasPositiveValue(format.getH()); - final boolean usesW = hasPositiveValue(format.getW()); - final boolean usesWmin = hasPositiveValue(format.getWmin()); - final boolean usesWratio = hasPositiveValue(format.getWratio()); - final boolean usesHratio = hasPositiveValue(format.getHratio()); - final boolean usesHW = usesH || usesW; - final boolean usesRatios = usesWmin || usesWratio || usesHratio; - - if (usesHW && usesRatios) { - throw new ValidationException("Request imp[%d].banner.format[%d] should define *either*" - + " {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" " - + "objects in the request", impIndex, formatIndex); - } - - if (!usesHW && !usesRatios) { - throw new ValidationException("Request imp[%d].banner.format[%d] should define *either*" - + " {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) " - + "to be non-zero positive", impIndex, formatIndex); - } - - if (usesHW && (!usesH || !usesW)) { - throw new ValidationException("Request imp[%d].banner.format[%d] must define a valid" - + " \"h\" and \"w\" properties", impIndex, formatIndex); - } - - if (usesRatios && (!usesWmin || !usesWratio || !usesHratio)) { - throw new ValidationException("Request imp[%d].banner.format[%d] must define a valid" - + " \"wmin\", \"wratio\", and \"hratio\" properties", impIndex, formatIndex); - } - } - - private void validateVideoMimes(Video video, int impIndex) throws ValidationException { - if (video != null) { - validateMimes(video.getMimes(), - "request.imp[%d].video.mimes must contain at least one supported MIME type", impIndex); - } - } - - private void validateAudioMimes(Audio audio, int impIndex) throws ValidationException { - if (audio != null) { - validateMimes(audio.getMimes(), - "request.imp[%d].audio.mimes must contain at least one supported MIME type", impIndex); - } - } - - private void validateMimes(List mimes, String msg, int index) throws ValidationException { - if (CollectionUtils.isEmpty(mimes)) { - throw new ValidationException(msg, index); - } - } - - private void validateMetrics(List metrics, int impIndex) throws ValidationException { - for (int i = 0; i < metrics.size(); i++) { - final Metric metric = metrics.get(i); - - if (StringUtils.isEmpty(metric.getType())) { - throw new ValidationException("Missing request.imp[%d].metric[%d].type", impIndex, i); - } - - final Float value = metric.getValue(); - if (value == null || value < 0.0 || value > 1.0) { - throw new ValidationException("request.imp[%d].metric[%d].value must be in the range [0.0, 1.0]", - impIndex, i); - } - } - } - - private static boolean hasPositiveValue(Integer value) { - return value != null && value > 0; - } } diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java index 17132e1bc35..4e2f062218b 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -11,6 +11,8 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; @@ -72,6 +74,7 @@ public ValidationResult validate(BidderBid bidderBid, final Bid bid = bidderBid.getBid(); final BidRequest bidRequest = auctionContext.getBidRequest(); final Account account = auctionContext.getAccount(); + final BidRejectionTracker bidRejectionTracker = auctionContext.getBidRejectionTrackers().get(bidder); final List warnings = new ArrayList<>(); try { @@ -81,10 +84,25 @@ public ValidationResult validate(BidderBid bidderBid, final Imp correspondingImp = findCorrespondingImp(bid, bidRequest); if (bidderBid.getType() == BidType.banner) { - warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases)); + warnings.addAll(validateBannerFields( + bidderBid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); } - warnings.addAll(validateSecureMarkup(bid, bidder, bidRequest, account, correspondingImp, aliases)); + warnings.addAll(validateSecureMarkup( + bidderBid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); + } catch (ValidationException e) { return ValidationResult.error(warnings, e.getMessage()); } @@ -143,17 +161,18 @@ private ValidationException exceptionAndLogOnePercent(String message) { return new ValidationException(message); } - private List validateBannerFields(Bid bid, + private List validateBannerFields(BidderBid bidderBid, String bidder, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { final BidValidationEnforcement bannerMaxSizeEnforcement = effectiveBannerMaxSizeEnforcement(account); if (bannerMaxSizeEnforcement != BidValidationEnforcement.skip) { final Format maxSize = maxSizeForBanner(correspondingImp); - + final Bid bid = bidderBid.getBid(); if (bannerSizeIsNotValid(bid, maxSize)) { final String accountId = account.getId(); final String message = """ @@ -174,7 +193,11 @@ private List validateBannerFields(Bid bid, bannerMaxSizeEnforcement, metricName -> metrics.updateSizeValidationMetrics( aliases.resolveBidder(bidder), accountId, metricName), - CREATIVE_SIZE_LOGGER, message); + CREATIVE_SIZE_LOGGER, + message, + bidRejectionTracker, + bidderBid, + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } } return Collections.emptyList(); @@ -213,12 +236,13 @@ private static boolean bannerSizeIsNotValid(Bid bid, Format maxSize) { || bidH == null || bidH > maxSize.getH(); } - private List validateSecureMarkup(Bid bid, + private List validateSecureMarkup(BidderBid bidderBid, String bidder, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { if (secureMarkupEnforcement == BidValidationEnforcement.skip) { return Collections.emptyList(); @@ -226,6 +250,7 @@ private List validateSecureMarkup(Bid bid, final String accountId = account.getId(); final String referer = getReferer(bidRequest); + final Bid bid = bidderBid.getBid(); final String adm = bid.getAdm(); if (isImpSecure(correspondingImp) && markupIsNotSecure(adm)) { @@ -238,7 +263,11 @@ private List validateSecureMarkup(Bid bid, secureMarkupEnforcement, metricName -> metrics.updateSecureValidationMetrics( aliases.resolveBidder(bidder), accountId, metricName), - SECURE_CREATIVE_LOGGER, message); + SECURE_CREATIVE_LOGGER, + message, + bidRejectionTracker, + bidderBid, + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } return Collections.emptyList(); @@ -253,12 +282,18 @@ private static boolean markupIsNotSecure(String adm) { || !StringUtils.containsAny(adm, SECURE_MARKUP_MARKERS); } - private List singleWarningOrValidationException(BidValidationEnforcement enforcement, - Consumer metricsRecorder, - ConditionalLogger conditionalLogger, - String message) throws ValidationException { + private List singleWarningOrValidationException( + BidValidationEnforcement enforcement, + Consumer metricsRecorder, + ConditionalLogger conditionalLogger, + String message, + BidRejectionTracker bidRejectionTracker, + BidderBid bidderBid, + BidRejectionReason bidRejectionReason) throws ValidationException { + return switch (enforcement) { case enforce -> { + bidRejectionTracker.rejectBid(bidderBid, bidRejectionReason); metricsRecorder.accept(MetricName.err); conditionalLogger.warn(message, logSamplingRate); throw new ValidationException(message); diff --git a/src/main/java/org/prebid/server/validation/ValidationException.java b/src/main/java/org/prebid/server/validation/ValidationException.java index 792d8a047bc..c5f4efae562 100644 --- a/src/main/java/org/prebid/server/validation/ValidationException.java +++ b/src/main/java/org/prebid/server/validation/ValidationException.java @@ -1,12 +1,12 @@ package org.prebid.server.validation; -class ValidationException extends Exception { +public class ValidationException extends Exception { - ValidationException(String errorMessageFormat) { + public ValidationException(String errorMessageFormat) { super(errorMessageFormat); } - ValidationException(String errorMessageFormat, Object... args) { + public ValidationException(String errorMessageFormat, Object... args) { super(errorMessageFormat.formatted(args)); } } diff --git a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java index e5aa90aabb2..7158bd4ba07 100644 --- a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java +++ b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java @@ -6,7 +6,7 @@ import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.Tuple; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; diff --git a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java index bb4fa7d09c1..ea59c9f9670 100644 --- a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java +++ b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java @@ -4,7 +4,7 @@ import io.vertx.core.Vertx; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; diff --git a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java index 87c9ada84c6..78a6a34ac7e 100644 --- a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java +++ b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java @@ -3,7 +3,7 @@ import io.vertx.core.Future; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import java.util.List; import java.util.function.Function; diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java b/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java index fe954cd3354..c6175ccaafa 100644 --- a/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java +++ b/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java @@ -33,12 +33,12 @@ public DaemonVerticle(List initializables, List startPromise) { - startPromise.handle(all(initializables, initializable -> initializable::initialize)); + all(initializables, initializable -> initializable::initialize).onComplete(startPromise); } @Override public void stop(Promise stopPromise) { - stopPromise.handle(all(closeables, closeable -> closeable::close)); + all(closeables, closeable -> closeable::close).onComplete(stopPromise); } private static Future all(Collection entries, diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 357a7a5220d..eb73cc16478 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -283,6 +283,8 @@ ipv6: anon-left-mask-bits: 56 private-networks: ::1/128, 2001:db8::/32, fc00::/7, fe80::/10, ff00::/8 analytics: + global: + adapters: logAnalytics, pubstack, greenbids, agmaAnalytics pubstack: enabled: false endpoint: http://localhost:8090 @@ -298,5 +300,17 @@ analytics: analytics-server: http://localhost:8090 exploratory-sampling-split: 0.9 timeout-ms: 10000 + agma: + enabled: false + accounts: + - code: code + publisher-id: pub + buffers: + size-bytes: 100000 + timeout-ms: 5000 + count: 4 + endpoint: + url: http:/url.com + timeout-ms: 5000 price-floors: enabled: false diff --git a/src/main/resources/bidder-config/aax.yaml b/src/main/resources/bidder-config/aax.yaml index b83b0a9bcf6..b695864b8c2 100644 --- a/src/main/resources/bidder-config/aax.yaml +++ b/src/main/resources/bidder-config/aax.yaml @@ -1,7 +1,7 @@ adapters: aax: endpoint: https://prebid.aaxads.com/rtb/pb/aax-prebid?src={{PREBID_SERVER_ENDPOINT}} - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: product@aax.media app-media-types: diff --git a/src/main/resources/bidder-config/adtonos.yaml b/src/main/resources/bidder-config/adtonos.yaml new file mode 100644 index 00000000000..e1a19fbc6eb --- /dev/null +++ b/src/main/resources/bidder-config/adtonos.yaml @@ -0,0 +1,22 @@ +adapters: + adtonos: + endpoint: https://exchange.adtonos.com/bid/{{PublisherId}} + geoscope: + - global + meta-info: + maintainer-email: support@adtonos.com + app-media-types: + - video + - audio + site-media-types: + - audio + dooh-media-types: + - audio + supported-vendors: + vendor-id: 682 + usersync: + cookie-family-name: adtonos + redirect: + url: https://play.adtonos.com/redir?to={{redirect_url}} + support-cors: false + uid-macro: '@UUID@' diff --git a/src/main/resources/bidder-config/aidem.yaml b/src/main/resources/bidder-config/aidem.yaml index cd8b37239ab..9cd7c5432af 100644 --- a/src/main/resources/bidder-config/aidem.yaml +++ b/src/main/resources/bidder-config/aidem.yaml @@ -1,7 +1,7 @@ adapters: aidem: endpoint: https://zero.aidemsrv.com/ortb/v2.6/bid/request?billing_id={{PublisherId}} - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: prebid@aidem.com app-media-types: diff --git a/src/main/resources/bidder-config/algorix.yaml b/src/main/resources/bidder-config/algorix.yaml index f76db6420ce..14d31df58d6 100644 --- a/src/main/resources/bidder-config/algorix.yaml +++ b/src/main/resources/bidder-config/algorix.yaml @@ -9,4 +9,4 @@ adapters: - native site-media-types: supported-vendors: - vendor-id: 0 + vendor-id: 1176 diff --git a/src/main/resources/bidder-config/bidmatic.yaml b/src/main/resources/bidder-config/bidmatic.yaml new file mode 100644 index 00000000000..47636ad0361 --- /dev/null +++ b/src/main/resources/bidder-config/bidmatic.yaml @@ -0,0 +1,13 @@ +adapters: + bidmatic: + endpoint: http://adapter.bidmatic.io/pbs/ortb + meta-info: + maintainer-email: advertising@bidmatic.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 1134 diff --git a/src/main/resources/bidder-config/blasto.yaml b/src/main/resources/bidder-config/blasto.yaml new file mode 100644 index 00000000000..d202f0acb2b --- /dev/null +++ b/src/main/resources/bidder-config/blasto.yaml @@ -0,0 +1,22 @@ +# Contact support@blasto.ai to connect with Blasto exchange. +# We have the following regional endpoint sub-domains: +# US East: t-us +# EU: t-eu +# APAC: t-apac +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +adapters: + blasto: + endpoint: http://t-us.blasto.ai/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} + endpoint-compression: gzip + meta-info: + maintainer-email: support@blasto.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/bluesea.yaml b/src/main/resources/bidder-config/bluesea.yaml index 91626852f12..23f6a7a702a 100644 --- a/src/main/resources/bidder-config/bluesea.yaml +++ b/src/main/resources/bidder-config/bluesea.yaml @@ -10,5 +10,8 @@ adapters: - video - native site-media-types: + - banner + - video + - native supported-vendors: - vendor-id: 0 + vendor-id: 1294 diff --git a/src/main/resources/bidder-config/consumable.yaml b/src/main/resources/bidder-config/consumable.yaml index 6f3f8f1660e..daf074a822f 100644 --- a/src/main/resources/bidder-config/consumable.yaml +++ b/src/main/resources/bidder-config/consumable.yaml @@ -1,6 +1,6 @@ adapters: consumable: - endpoint: https://e.serverbid.com + endpoint: https://e.serverbid.com/api/v2 meta-info: maintainer-email: prebid@consumable.com app-media-types: diff --git a/src/main/resources/bidder-config/copper6ssp.yaml b/src/main/resources/bidder-config/copper6ssp.yaml new file mode 100644 index 00000000000..bc7ceceb4b4 --- /dev/null +++ b/src/main/resources/bidder-config/copper6ssp.yaml @@ -0,0 +1,25 @@ +adapters: + copper6ssp: + endpoint: https://endpoint.copper6.com/ + meta-info: + maintainer-email: info@copper6.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: copper6ssp + redirect: + support-cors: false + url: https://csync.copper6.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' + iframe: + support-cors: false + url: https://csync.copper6.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/driftpixel.yaml b/src/main/resources/bidder-config/driftpixel.yaml index 384d75db00c..5bba4d9257f 100644 --- a/src/main/resources/bidder-config/driftpixel.yaml +++ b/src/main/resources/bidder-config/driftpixel.yaml @@ -13,3 +13,9 @@ adapters: - native supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: driftpixel + redirect: + url: "https://sync.driftpixel.live/psync?t=s&e=0&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb={{redirect_url}}" + support-cors: false + uid-macro: "%USER_ID%" diff --git a/src/main/resources/bidder-config/epsilon.yaml b/src/main/resources/bidder-config/epsilon.yaml index 27d1ed14343..ba7472331d7 100644 --- a/src/main/resources/bidder-config/epsilon.yaml +++ b/src/main/resources/bidder-config/epsilon.yaml @@ -11,9 +11,11 @@ adapters: app-media-types: - banner - video + - audio site-media-types: - banner - video + - audio supported-vendors: vendor-id: 24 usersync: diff --git a/src/main/resources/bidder-config/escalax.yaml b/src/main/resources/bidder-config/escalax.yaml new file mode 100644 index 00000000000..8c6c44dbdea --- /dev/null +++ b/src/main/resources/bidder-config/escalax.yaml @@ -0,0 +1,17 @@ +adapters: + escalax: + endpoint: http://bidder_us.escalax.io/?partner={{.SourceId}}&token={{.AccountID}}&type=pbs + modifying-vast-xml-allowed: true + endpoint-compression: gzip + meta-info: + maintainer-email: connect@escalax.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/freewheelssp.yaml b/src/main/resources/bidder-config/freewheelssp.yaml index 1756a47cbd7..b198a837a9c 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -1,7 +1,8 @@ adapters: freewheelssp: endpoint: https://ads.stickyadstv.com/openrtb/dsp - modifyingVastXmlAllowed: true + ortb-version: "2.6" + modifying-vast-xml-allowed: true meta-info: maintainer-email: prebid-maintainer@freewheel.com app-media-types: diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index 172069d1a2e..06ec164dfd9 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -41,21 +41,6 @@ adapters: - video supported-vendors: vendor-id: 0 - loopme: - enabled: false - endpoint: http://prebid-eu.loopmertb.com - meta-info: - maintainer-email: support@loopme.com - app-media-types: - - banner - - video - - native - site-media-types: - - banner - - video - - native - supported-vendors: - vendor-id: 109 zeta_global_ssp: enabled: false endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA @@ -129,15 +114,15 @@ adapters: - video - native site-media-types: - - banner - - video - - native + - banner + - video + - native supported-vendors: vendor-id: 263 usersync: cookie-family-name: nativo redirect: - url: http://jadserve.postrelease.com/suid/101787?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&ntv_gpp_consent={{gpp}}&ntv_r={{redirect_url}} + url: https://jadserve.postrelease.com/suid/101787?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&ntv_gpp_consent={{gpp}}&ntv_r={{redirect_url}} support-cors: false uid-macro: 'NTV_USER_ID' meta-info: diff --git a/src/main/resources/bidder-config/gumgum.yaml b/src/main/resources/bidder-config/gumgum.yaml index e6bd1d4e98e..b3ee922dce4 100644 --- a/src/main/resources/bidder-config/gumgum.yaml +++ b/src/main/resources/bidder-config/gumgum.yaml @@ -1,6 +1,7 @@ adapters: gumgum: endpoint: https://g2.gumgum.com/providers/prbds2s/bid + ortb-version: "2.6" meta-info: maintainer-email: prebid@gumgum.com app-media-types: diff --git a/src/main/resources/bidder-config/iqzone.yaml b/src/main/resources/bidder-config/iqzone.yaml index ccd2f126f3f..a8292c5033a 100644 --- a/src/main/resources/bidder-config/iqzone.yaml +++ b/src/main/resources/bidder-config/iqzone.yaml @@ -13,3 +13,13 @@ adapters: - native supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: iqzone + redirect: + url: https://cs.iqzone.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + iframe: + url: https://cs.iqzone.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/kargo.yaml b/src/main/resources/bidder-config/kargo.yaml index 164eb0a7916..56d5e9ea22d 100644 --- a/src/main/resources/bidder-config/kargo.yaml +++ b/src/main/resources/bidder-config/kargo.yaml @@ -1,8 +1,9 @@ adapters: kargo: endpoint: https://krk.kargo.com/api/v1/openrtb + ortb-version: "2.6" endpoint-compression: gzip - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: kraken@kargo.com app-media-types: diff --git a/src/main/resources/bidder-config/krushmedia.yaml b/src/main/resources/bidder-config/krushmedia.yaml index 46b2d2aa47e..0f2a42e7785 100644 --- a/src/main/resources/bidder-config/krushmedia.yaml +++ b/src/main/resources/bidder-config/krushmedia.yaml @@ -16,6 +16,10 @@ adapters: usersync: cookie-family-name: krushmedia redirect: - url: https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}} + url: https://cs.krushmedia.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} support-cors: false - uid-macro: '[uid]' + uid-macro: '[UID]' + iframe: + url: https://cs.krushmedia.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/lemmadigital.yaml b/src/main/resources/bidder-config/lemmadigital.yaml index 1d7208aff4f..3f71b2d016c 100644 --- a/src/main/resources/bidder-config/lemmadigital.yaml +++ b/src/main/resources/bidder-config/lemmadigital.yaml @@ -1,6 +1,6 @@ adapters: lemmadigital: - endpoint: https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{PublisherID}}&aid={{AdUnit}} + endpoint: https://pbid.lemmamedia.com/lemma/servad?src=prebid&pid={{PublisherID}}&aid={{AdUnit}} meta-info: maintainer-email: support@lemmatechnologies.com endpoint-compression: gzip @@ -13,3 +13,9 @@ adapters: - video supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: lemmadigital + redirect: + url: https://sync.lemmadigital.com/setuid?publisher=850&redirect={{redirect_url}} + support-cors: false + uid-macro: '${UUID}' diff --git a/src/main/resources/bidder-config/limelightDigital.yaml b/src/main/resources/bidder-config/limelightDigital.yaml index ab2336287e0..48c238c3840 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -2,6 +2,8 @@ adapters: limelightDigital: endpoint: http://ads-pbs.ortb.net/openrtb/{{PublisherID}}?host={{Host}} aliases: + filmzie: + enabled: false iionads: enabled: false endpoint: http://ads-pbs.iionads.com/openrtb/{{PublisherID}}?host={{Host}} @@ -34,6 +36,11 @@ adapters: embimedia: enabled: false endpoint: http://ads-pbs.bidder-embi.media/openrtb/{{PublisherID}}?host={{Host}} + tgm: + enabled: false + streamlyn: + enabled: false + endpoint: http://rtba.bidsxchange.com/openrtb/{{PublisherID}}?host={{Host}} meta-info: maintainer-email: engineering@project-limelight.com app-media-types: diff --git a/src/main/resources/bidder-config/loopme.yaml b/src/main/resources/bidder-config/loopme.yaml new file mode 100644 index 00000000000..bee027b1b18 --- /dev/null +++ b/src/main/resources/bidder-config/loopme.yaml @@ -0,0 +1,23 @@ +adapters: + loopme: + endpoint: http://prebid.loopmertb.com + meta-info: + maintainer-email: prebid@loopme.com + app-media-types: + - banner + - video + - audio + - native + site-media-types: + - banner + - video + - audio + - native + supported-vendors: + vendor-id: 109 + usersync: + url: https://csync.loopme.me/?pubid=11393&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + redirect-url: /setuid?bidder=loopme&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={udid} + cookie-family-name: loopme + type: redirect + support-cors: false diff --git a/src/main/resources/bidder-config/medianet.yaml b/src/main/resources/bidder-config/medianet.yaml index 63aecb5f2b0..ceb545af062 100644 --- a/src/main/resources/bidder-config/medianet.yaml +++ b/src/main/resources/bidder-config/medianet.yaml @@ -17,6 +17,10 @@ adapters: vendor-id: 142 usersync: cookie-family-name: medianet + iframe: + url: https://hbx.media.net/checksync.php?cid=8CUEHS6F9&cs=87&type=mpbc&cv=37&vsSync=1&uspstring={{us_privacy}}&gdpr={{gdpr}}&gdprsting={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '' redirect: url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false diff --git a/src/main/resources/bidder-config/melozen.yaml b/src/main/resources/bidder-config/melozen.yaml new file mode 100644 index 00000000000..8626fd4eabe --- /dev/null +++ b/src/main/resources/bidder-config/melozen.yaml @@ -0,0 +1,19 @@ +adapters: + melozen: + endpoint: https://prebid.melozen.com/rtb/v2/bid?publisher_id={{PublisherID}} + endpoint-compression: gzip + modifying-vast-xml-allowed: true + geoscope: + - global + meta-info: + maintainer-email: DSP@melodong.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/metax.yaml b/src/main/resources/bidder-config/metax.yaml new file mode 100644 index 00000000000..7e87526ac0b --- /dev/null +++ b/src/main/resources/bidder-config/metax.yaml @@ -0,0 +1,18 @@ +# The MetaX Bidding adapter requires setup before beginning. Please contact us at +adapters: + metax: + endpoint: https://hb.metaxads.com/prebid?sid={{publisherId}}&adunit={{adUnit}}&source=prebid-server + meta-info: + maintainer-email: prebid@metaxsoft.com + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 1301 diff --git a/src/main/resources/bidder-config/missena.yaml b/src/main/resources/bidder-config/missena.yaml new file mode 100644 index 00000000000..4f1ea9e4f8f --- /dev/null +++ b/src/main/resources/bidder-config/missena.yaml @@ -0,0 +1,18 @@ +adapters: + missena: + endpoint: https://bid.missena.io/ + meta-info: + maintainer-email: prebid@missena.com + modifying-vast-xml-allowed: true + app-media-types: + - banner + site-media-types: + - banner + supported-vendors: + vendor-id: 687 + usersync: + cookie-family-name: missena + iframe: + url: https://sync.missena.io/iframe?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml index e16a4ac1210..ea6645feb96 100644 --- a/src/main/resources/bidder-config/mobilefuse.yaml +++ b/src/main/resources/bidder-config/mobilefuse.yaml @@ -1,6 +1,7 @@ adapters: mobilefuse: endpoint: http://mfx.mobilefuse.com/openrtb?pub_id= + ortb-version: "2.6" endpoint-compression: gzip # This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions: geoscope: diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml index 78aa8020d30..9e8454131d4 100644 --- a/src/main/resources/bidder-config/openx.yaml +++ b/src/main/resources/bidder-config/openx.yaml @@ -1,6 +1,7 @@ adapters: openx: endpoint: http://rtb.openx.net/prebid + ortb-version: "2.6" endpoint-compression: gzip meta-info: maintainer-email: prebid@openx.com diff --git a/src/main/resources/bidder-config/oraki.yaml b/src/main/resources/bidder-config/oraki.yaml new file mode 100644 index 00000000000..f5197ac83ff --- /dev/null +++ b/src/main/resources/bidder-config/oraki.yaml @@ -0,0 +1,21 @@ +adapters: + oraki: + endpoint: https://eu1.oraki.io/pserver + meta-info: + maintainer-email: prebid@oraki.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: oraki + redirect: + support-cors: false + url: https://sync.oraki.io/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/ownadx.yaml b/src/main/resources/bidder-config/ownadx.yaml new file mode 100644 index 00000000000..c836d847f98 --- /dev/null +++ b/src/main/resources/bidder-config/ownadx.yaml @@ -0,0 +1,20 @@ +adapters: + ownadx: + endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{SeatID}}/{{SspID}}?token={{TokenID}}" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid-team@techbravo.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: ownadx + redirect: + url: https://sync.spoutroserve.com/user-sync?t=image&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&s3={{redirect_url}} + support-cors: false + uid-macro: '{USER_ID}' diff --git a/src/main/resources/bidder-config/pgamssp.yaml b/src/main/resources/bidder-config/pgamssp.yaml index e2088e63e0e..70105cab774 100644 --- a/src/main/resources/bidder-config/pgamssp.yaml +++ b/src/main/resources/bidder-config/pgamssp.yaml @@ -12,7 +12,7 @@ adapters: - video - native supported-vendors: - vendor-id: 0 + vendor-id: 1353 usersync: cookie-family-name: pgamssp redirect: diff --git a/src/main/resources/bidder-config/playdigo.yaml b/src/main/resources/bidder-config/playdigo.yaml index 6bc464fea1b..adb14424c1b 100644 --- a/src/main/resources/bidder-config/playdigo.yaml +++ b/src/main/resources/bidder-config/playdigo.yaml @@ -14,4 +14,14 @@ adapters: - video - native supported-vendors: - vendor-id: 0 + vendor-id: 1302 + usersync: + cookie-family-name: playdigo + redirect: + url: https://cs.playdigo.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + iframe: + url: https://cs.playdigo.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/pubmatic.yaml b/src/main/resources/bidder-config/pubmatic.yaml index 8f59f1912f1..5ca8ad94339 100644 --- a/src/main/resources/bidder-config/pubmatic.yaml +++ b/src/main/resources/bidder-config/pubmatic.yaml @@ -1,6 +1,7 @@ adapters: pubmatic: endpoint: https://hbopenbid.pubmatic.com/translator?source=prebid-server + ortb-version: "2.6" meta-info: maintainer-email: header-bidding@pubmatic.com app-media-types: diff --git a/src/main/resources/bidder-config/pubrise.yaml b/src/main/resources/bidder-config/pubrise.yaml new file mode 100644 index 00000000000..ae7768d44b2 --- /dev/null +++ b/src/main/resources/bidder-config/pubrise.yaml @@ -0,0 +1,25 @@ +adapters: + pubrise: + endpoint: https://backend.pubrise.ai/ + meta-info: + maintainer-email: prebid@pubrise.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: pubrise + redirect: + support-cors: false + url: https://sync.pubrise.ai/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' + iframe: + support-cors: false + url: https://sync.pubrise.ai/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/pulsepoint.yaml b/src/main/resources/bidder-config/pulsepoint.yaml index 3e1d27ee281..8cd407e3b74 100644 --- a/src/main/resources/bidder-config/pulsepoint.yaml +++ b/src/main/resources/bidder-config/pulsepoint.yaml @@ -1,6 +1,7 @@ adapters: pulsepoint: endpoint: http://bid.contextweb.com/header/s/ortb/prebid-s2s + ortb-version: "2.6" meta-info: maintainer-email: ExchangeTeam@pulsepoint.com app-media-types: diff --git a/src/main/resources/bidder-config/qt.yaml b/src/main/resources/bidder-config/qt.yaml new file mode 100644 index 00000000000..8c81123c8ca --- /dev/null +++ b/src/main/resources/bidder-config/qt.yaml @@ -0,0 +1,21 @@ +adapters: + qt: + endpoint: https://endpoint1.qt.io/pserver + meta-info: + maintainer-email: qtssp-support@qt.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1331 + usersync: + cookie-family-name: qt + redirect: + support-cors: false + url: https://cs.qt.io/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/richaudience.yaml b/src/main/resources/bidder-config/richaudience.yaml index ebe84aec464..b691d330734 100644 --- a/src/main/resources/bidder-config/richaudience.yaml +++ b/src/main/resources/bidder-config/richaudience.yaml @@ -17,3 +17,7 @@ adapters: url: https://sync.richaudience.com/74889303289e27f327ad0c6de7be7264/?consentString={{gdpr_consent}}&r={{redirect_url}} support-cors: false uid-macro: '[PDID]' + redirect: + url: https://sync.richaudience.com/f7872c90c5d3791e2b51f7edce1a0a5d/?p=pbs&consentString={{gdpr_consent}}&r={{redirect_url}} + support-cors: false + uid-macro: '[PDID]' diff --git a/src/main/resources/bidder-config/rubicon.yaml b/src/main/resources/bidder-config/rubicon.yaml index 7f886ab5634..882732f83a5 100644 --- a/src/main/resources/bidder-config/rubicon.yaml +++ b/src/main/resources/bidder-config/rubicon.yaml @@ -11,6 +11,8 @@ adapters: aliases: magnite: enabled: false + ortb: + multiformat-supported: true meta-info: maintainer-email: header-bidding@rubiconproject.com app-media-types: @@ -36,7 +38,6 @@ adapters: url: GET_FROM_globalsupport@magnite.com support-cors: false generate-bid-id: false - use-video-size-id-logic: true XAPI: Username: GET_FROM_globalsupport@magnite.com Password: GET_FROM_globalsupport@magnite.com diff --git a/src/main/resources/bidder-config/sharethrough.yaml b/src/main/resources/bidder-config/sharethrough.yaml index 36cc0f0127f..77bcce9d31f 100644 --- a/src/main/resources/bidder-config/sharethrough.yaml +++ b/src/main/resources/bidder-config/sharethrough.yaml @@ -1,6 +1,7 @@ adapters: sharethrough: endpoint: https://btlr.sharethrough.com/universal/v1?supply_id=FGMrCMMc + ortb-version: '2.6' meta-info: maintainer-email: pubgrowth.engineering@sharethrough.com app-media-types: diff --git a/src/main/resources/bidder-config/smarthub.yaml b/src/main/resources/bidder-config/smarthub.yaml index 8d8ec17d1dd..ebe51135d06 100644 --- a/src/main/resources/bidder-config/smarthub.yaml +++ b/src/main/resources/bidder-config/smarthub.yaml @@ -11,6 +11,12 @@ adapters: tredio: enabled: false endpoint: http://tredio-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + vimayx: + enabled: false + endpoint: http://vimayx-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + felixads: + enabled: false + endpoint: http://felixads-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} meta-info: maintainer-email: support@smart-hub.io app-media-types: diff --git a/src/main/resources/bidder-config/smartx.yaml b/src/main/resources/bidder-config/smartx.yaml index 50a731d8c9d..d0f4498a32a 100644 --- a/src/main/resources/bidder-config/smartx.yaml +++ b/src/main/resources/bidder-config/smartx.yaml @@ -1,6 +1,7 @@ adapters: smartx: endpoint: https://bid.smartclip.net/bid/1005 + ortb-version: "2.6" meta-info: maintainer-email: bidding@smartclip.tv app-media-types: diff --git a/src/main/resources/bidder-config/sonobi.yaml b/src/main/resources/bidder-config/sonobi.yaml index 1c00983ec39..c6405555f1a 100644 --- a/src/main/resources/bidder-config/sonobi.yaml +++ b/src/main/resources/bidder-config/sonobi.yaml @@ -6,13 +6,19 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 104 usersync: cookie-family-name: sonobi + iframe: + url: https://sync.go.sonobi.com/uc.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&loc={{redirect_url}} + support-cors: false + uid-macro: '[UID]' redirect: url: https://sync.go.sonobi.com/us.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&loc={{redirect_url}} support-cors: false diff --git a/src/main/resources/bidder-config/sovrn.yaml b/src/main/resources/bidder-config/sovrn.yaml index d2d22c55c21..8f74ae266b6 100644 --- a/src/main/resources/bidder-config/sovrn.yaml +++ b/src/main/resources/bidder-config/sovrn.yaml @@ -1,7 +1,8 @@ adapters: sovrn: endpoint: http://ap.lijit.com/rtb/bid?src=prebid_server - modifyingVastXmlAllowed: true + endpoint-compression: gzip + modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com app-media-types: diff --git a/src/main/resources/bidder-config/sovrnXsp.yaml b/src/main/resources/bidder-config/sovrnXsp.yaml index 706a06caafe..6a2a626e66f 100644 --- a/src/main/resources/bidder-config/sovrnXsp.yaml +++ b/src/main/resources/bidder-config/sovrnXsp.yaml @@ -2,7 +2,7 @@ adapters: sovrnXsp: endpoint: http://xsp.lijit.com/json/rtb/prebid/server endpoint-compression: gzip - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com app-media-types: diff --git a/src/main/resources/bidder-config/bizzclick.yaml b/src/main/resources/bidder-config/thetradedesk.yaml similarity index 52% rename from src/main/resources/bidder-config/bizzclick.yaml rename to src/main/resources/bidder-config/thetradedesk.yaml index f5037c1014a..b71a0e38cc2 100644 --- a/src/main/resources/bidder-config/bizzclick.yaml +++ b/src/main/resources/bidder-config/thetradedesk.yaml @@ -1,8 +1,8 @@ adapters: - bizzclick: - endpoint: http://{{Host}}.bizzclick.com/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} + thetradedesk: + endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}} meta-info: - maintainer-email: support@bizzclick.com + maintainer-email: Prebid-Maintainers@thetradedesk.com app-media-types: - banner - video @@ -12,4 +12,4 @@ adapters: - video - native supported-vendors: - vendor-id: 0 + vendor-id: 21 diff --git a/src/main/resources/bidder-config/tradplus.yaml b/src/main/resources/bidder-config/tradplus.yaml new file mode 100644 index 00000000000..9644f025c45 --- /dev/null +++ b/src/main/resources/bidder-config/tradplus.yaml @@ -0,0 +1,11 @@ +adapters: + tradplus: + endpoint: "https://{{ZoneID}}adx.tradplusad.com/{{AccountID}}/pserver" + meta-info: + maintainer-email: "tpxcontact@tradplus.com" + app-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/trafficgate.yaml b/src/main/resources/bidder-config/trafficgate.yaml index e4dd6b1fcd6..135d61e2fbe 100644 --- a/src/main/resources/bidder-config/trafficgate.yaml +++ b/src/main/resources/bidder-config/trafficgate.yaml @@ -1,7 +1,7 @@ adapters: trafficgate: endpoint: http://{{subdomain}}.bc-plugin.com/?c=o&m=rtb - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: "support@bidscube.com" app-media-types: diff --git a/src/main/resources/bidder-config/triplelift.yaml b/src/main/resources/bidder-config/triplelift.yaml index e8c35e3eb2c..446825e33dc 100644 --- a/src/main/resources/bidder-config/triplelift.yaml +++ b/src/main/resources/bidder-config/triplelift.yaml @@ -1,6 +1,7 @@ adapters: triplelift: endpoint: https://tlx.3lift.com/s2s/auction?sra=1&supplier_id=20 + ortb-version: "2.6" endpoint-compression: gzip meta-info: maintainer-email: prebid@triplelift.com diff --git a/src/main/resources/bidder-config/tripleliftnative.yaml b/src/main/resources/bidder-config/tripleliftnative.yaml index b090925ff82..e6c1f106f62 100644 --- a/src/main/resources/bidder-config/tripleliftnative.yaml +++ b/src/main/resources/bidder-config/tripleliftnative.yaml @@ -1,6 +1,7 @@ adapters: triplelift_native: endpoint: https://tlx.3lift.com/s2sn/auction?supplier_id=20 + ortb-version: "2.6" meta-info: maintainer-email: prebid@triplelift.com app-media-types: diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml index 050c61c62d9..0b239b0a3f7 100644 --- a/src/main/resources/bidder-config/unruly.yaml +++ b/src/main/resources/bidder-config/unruly.yaml @@ -1,6 +1,7 @@ adapters: unruly: endpoint: https://targeting.unrulymedia.com/unruly_prebid_server + ortb-version: "2.6" meta-info: maintainer-email: prebidsupport@unrulygroup.com app-media-types: diff --git a/src/main/resources/bidder-config/vidazoo.yaml b/src/main/resources/bidder-config/vidazoo.yaml index 727c4d74da5..f99cbb0ee1e 100644 --- a/src/main/resources/bidder-config/vidazoo.yaml +++ b/src/main/resources/bidder-config/vidazoo.yaml @@ -15,6 +15,6 @@ adapters: usersync: cookie-family-name: vidazoo iframe: - url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} support-cors: false uid-macro: '${userId}' diff --git a/src/main/resources/bidder-config/liftoff.yaml b/src/main/resources/bidder-config/vungle.yaml similarity index 83% rename from src/main/resources/bidder-config/liftoff.yaml rename to src/main/resources/bidder-config/vungle.yaml index 5b415c8b84a..0a9baf58403 100644 --- a/src/main/resources/bidder-config/liftoff.yaml +++ b/src/main/resources/bidder-config/vungle.yaml @@ -1,6 +1,9 @@ adapters: - liftoff: + vungle: endpoint: https://rtb.ads.vungle.com/bid/t/c770f32 + aliases: + liftoff: + enabled: false modifying-vast-xml-allowed: true endpoint-compression: gzip meta-info: diff --git a/src/main/resources/bidder-config/yieldmo.yaml b/src/main/resources/bidder-config/yieldmo.yaml index 384804a3722..84415ead13e 100644 --- a/src/main/resources/bidder-config/yieldmo.yaml +++ b/src/main/resources/bidder-config/yieldmo.yaml @@ -1,6 +1,7 @@ adapters: yieldmo: endpoint: https://ads.yieldmo.com/exchange/prebid-server + ortb-version: "2.6" meta-info: maintainer-email: prebid@yieldmo.com app-media-types: diff --git a/src/main/resources/static/bidder-params/adtelligent.json b/src/main/resources/static/bidder-params/adtelligent.json index db7931e1ec0..e8dedf33690 100644 --- a/src/main/resources/static/bidder-params/adtelligent.json +++ b/src/main/resources/static/bidder-params/adtelligent.json @@ -2,7 +2,6 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Adtelligent Adapter Params", "description": "A schema which validates params accepted by the Adtelligent adapter", - "type": "object", "properties": { "placementId": { @@ -14,7 +13,10 @@ "description": "An ID which identifies the site selling the impression" }, "aid": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "An ID which identifies the channel" }, "bidFloor": { diff --git a/src/main/resources/static/bidder-params/adtonos.json b/src/main/resources/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..b1ea833f1e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/adtonos.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": [ + "supplierId" + ] +} diff --git a/src/main/resources/static/bidder-params/bidmatic.json b/src/main/resources/static/bidder-params/bidmatic.json new file mode 100644 index 00000000000..65a1309dafa --- /dev/null +++ b/src/main/resources/static/bidder-params/bidmatic.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidmatic Adapter Params", + "description": "A schema which validates params accepted by the Bidmatic 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" + }, + "source": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": [ + "source" + ] +} diff --git a/src/main/resources/static/bidder-params/bizzclick.json b/src/main/resources/static/bidder-params/blasto.json similarity index 72% rename from src/main/resources/static/bidder-params/bizzclick.json rename to src/main/resources/static/bidder-params/blasto.json index 879ab45314f..23109fb2421 100644 --- a/src/main/resources/static/bidder-params/bizzclick.json +++ b/src/main/resources/static/bidder-params/blasto.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Bizzclick Adapter Params", - "description": "A schema which validates params accepted by the Bizzclick adapter", + "title": "Blasto Adapter Params", + "description": "A schema which validates params accepted by the Blasto adapter", "type": "object", "properties": { "accountId": { @@ -9,14 +9,14 @@ "description": "Account id", "minLength": 1 }, - "placementId": { + "sourceId": { "type": "string", - "description": "PlacementId id", + "description": "Source id", "minLength": 1 } }, "required": [ "accountId", - "placementId" + "sourceId" ] } diff --git a/src/main/resources/static/bidder-params/copper6ssp.json b/src/main/resources/static/bidder-params/copper6ssp.json new file mode 100644 index 00000000000..e17c3f38ce7 --- /dev/null +++ b/src/main/resources/static/bidder-params/copper6ssp.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Copper6SSPs Adapter Params", + "description": "A schema which validates params accepted by the Copper6SSP 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" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/escalax.json b/src/main/resources/static/bidder-params/escalax.json new file mode 100644 index 00000000000..68fda39c259 --- /dev/null +++ b/src/main/resources/static/bidder-params/escalax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Escalax Adapter Params", + "description": "A schema which validates params accepted by the Escalax adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "description": "Source id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "sourceId" + ] +} diff --git a/src/main/resources/static/bidder-params/improvedigital.json b/src/main/resources/static/bidder-params/improvedigital.json index 5681d896e92..ecd60a98b1d 100644 --- a/src/main/resources/static/bidder-params/improvedigital.json +++ b/src/main/resources/static/bidder-params/improvedigital.json @@ -35,5 +35,7 @@ "description": "Placement size" } }, - "required": ["placementId"] + "required": [ + "placementId" + ] } diff --git a/src/main/resources/static/bidder-params/loopme.json b/src/main/resources/static/bidder-params/loopme.json index f6b4a0a8b2e..5ea22ec7ba5 100644 --- a/src/main/resources/static/bidder-params/loopme.json +++ b/src/main/resources/static/bidder-params/loopme.json @@ -3,13 +3,22 @@ "title": "Loopme Adapter Params", "description": "A schema which validates params accepted by the Loopme adapter", "type": "object", - "properties": { - "accountId": { + "publisherId": { "type": "string", - "description": "Account ID" + "description": "An id which identifies Loopme partner", + "minLength": 1 + }, + "bundleId": { + "type": "string", + "description": "An id which identifies app/site in Loopme", + "minLength": 1 + }, + "placementId": { + "type": "string", + "description": "A placement id in Loopme", + "minLength": 1 } }, - - "required": ["accountId"] -} \ No newline at end of file + "required": ["publisherId"] +} diff --git a/src/main/resources/static/bidder-params/melozen.json b/src/main/resources/static/bidder-params/melozen.json new file mode 100644 index 00000000000..eebd391944b --- /dev/null +++ b/src/main/resources/static/bidder-params/melozen.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MeloZen Adapter Params", + "description": "A schema which validates params accepted by the MeloZen adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "minLength": 1, + "description": "The unique identifier for the publisher." + } + }, + "required": [ + "pubId" + ] +} diff --git a/src/main/resources/static/bidder-params/metax.json b/src/main/resources/static/bidder-params/metax.json new file mode 100644 index 00000000000..5e65b5c4e2b --- /dev/null +++ b/src/main/resources/static/bidder-params/metax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MetaX Adapter Params", + "description": "A schema which validates params accepted by the MetaX adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "integer", + "description": "An ID which identifies the publisher", + "minimum": 1 + }, + "adunit": { + "type": "integer", + "description": "An ID which identifies the adunit", + "minimum": 1 + } + }, + "required": [ + "publisherId", + "adunit" + ] +} diff --git a/src/main/resources/static/bidder-params/missena.json b/src/main/resources/static/bidder-params/missena.json new file mode 100644 index 00000000000..86bf5b45dec --- /dev/null +++ b/src/main/resources/static/bidder-params/missena.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Missena Adapter Params", + "description": "A schema which validates params accepted by the Missena adapter", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "API Key", + "minLength": 1 + }, + "placement": { + "type": "string", + "description": "Placement Type (Sticky, Header, ...)" + }, + "test": { + "type": "string", + "description": "Test Mode" + } + }, + "required": [ + "apiKey" + ] +} diff --git a/src/main/resources/static/bidder-params/oraki.json b/src/main/resources/static/bidder-params/oraki.json new file mode 100644 index 00000000000..9a2d596eeff --- /dev/null +++ b/src/main/resources/static/bidder-params/oraki.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Oraki Adapter Params", + "description": "A schema which validates params accepted by the Oraki 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" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/ownadx.json b/src/main/resources/static/bidder-params/ownadx.json new file mode 100644 index 00000000000..fae8689bf55 --- /dev/null +++ b/src/main/resources/static/bidder-params/ownadx.json @@ -0,0 +1,25 @@ +{ + "$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" + } + }, + "required": [ + "sspId", + "seatId", + "tokenId" + ] +} diff --git a/src/main/resources/static/bidder-params/pubrise.json b/src/main/resources/static/bidder-params/pubrise.json new file mode 100644 index 00000000000..9dd2a1e4c80 --- /dev/null +++ b/src/main/resources/static/bidder-params/pubrise.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Pubrise Adapter Params", + "description": "A schema which validates params accepted by the Pubrise 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" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/qt.json b/src/main/resources/static/bidder-params/qt.json new file mode 100644 index 00000000000..ef7eb77a9ac --- /dev/null +++ b/src/main/resources/static/bidder-params/qt.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "QT Adapter Params", + "description": "A schema which validates params accepted by the QT 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" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/sovrn.json b/src/main/resources/static/bidder-params/sovrn.json index 803a8e127a1..4f779a9f1f6 100644 --- a/src/main/resources/static/bidder-params/sovrn.json +++ b/src/main/resources/static/bidder-params/sovrn.json @@ -13,8 +13,16 @@ "description": "An ID which identifies the sovrn ad tag (DEPRECATED, use \"tagid\" instead)" }, "bidfloor": { - "type": "number", - "description": "The minimum acceptable bid, in CPM, using US Dollars" + "anyOf": [ + { + "type": "number", + "description": "The minimum acceptable bid, in CPM, using US Dollars" + }, + { + "type": "string", + "description": "The minimum acceptable bid, in CPM, using US Dollars (as a string)" + } + ] }, "adunitcode": { "type": "string", diff --git a/src/main/resources/static/bidder-params/thetradedesk.json b/src/main/resources/static/bidder-params/thetradedesk.json new file mode 100644 index 00000000000..d0b305a5a1e --- /dev/null +++ b/src/main/resources/static/bidder-params/thetradedesk.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "The Trade Desk Adapter Params", + "description": "A schema which validates params accepted by the The Trade Desk adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "An ID which identifies the publisher" + } + }, + "required": [ + "publisherId" + ] +} diff --git a/src/main/resources/static/bidder-params/tradplus.json b/src/main/resources/static/bidder-params/tradplus.json new file mode 100644 index 00000000000..deae1392d1d --- /dev/null +++ b/src/main/resources/static/bidder-params/tradplus.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "TradPlus Adapter Params", + "description": "A schema which validates params accepted by the TradPlus adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone ID" + } + }, + "required": [ + "accountId", + "zoneId" + ] +} diff --git a/src/main/resources/static/bidder-params/liftoff.json b/src/main/resources/static/bidder-params/vungle.json similarity index 89% rename from src/main/resources/static/bidder-params/liftoff.json rename to src/main/resources/static/bidder-params/vungle.json index 5664a883b9e..e2d4dddffdc 100644 --- a/src/main/resources/static/bidder-params/liftoff.json +++ b/src/main/resources/static/bidder-params/vungle.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Liftoff Adapter Params", - "description": "A schema which validates params accepted by the Liftoff adapter", + "title": "Vungle Adapter Params", + "description": "A schema which validates params accepted by the Vungle adapter", "type": "object", "properties": { "app_store_id": { diff --git a/src/main/resources/static/bidder-params/yieldlab.json b/src/main/resources/static/bidder-params/yieldlab.json index 900d65da6e5..9d0fd0e88c0 100644 --- a/src/main/resources/static/bidder-params/yieldlab.json +++ b/src/main/resources/static/bidder-params/yieldlab.json @@ -12,10 +12,6 @@ "type": "string", "description": "Yieldlab ID of the supply" }, - "adSize": { - "type": "string", - "description": "Size of the adslot in pixel, e.g. 200x50" - }, "extId": { "type": "string", "description": "External ID used for reporting" @@ -27,7 +23,6 @@ }, "required": [ "adslotId", - "supplyId", - "adSize" + "supplyId" ] } diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index eedb8412ba5..2bc06ab7144 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -4,8 +4,10 @@ import com.fasterxml.jackson.annotation.JsonValue enum ModuleName { - PB_RICHMEDIA_FILTER('pb-richmedia-filter'), - ORTB2_BLOCKING('ortb2-blocking') + PB_RICHMEDIA_FILTER("pb-richmedia-filter"), + PB_RESPONSE_CORRECTION ("pb-response-correction"), + ORTB2_BLOCKING("ortb2-blocking"), + PB_REQUEST_CORRECTION('pb-request-correction'), @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy index afdd81c19ef..f91f209395c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy @@ -10,6 +10,7 @@ enum BidderName { EMPTY(""), BOGUS("bogus"), ALIAS("alias"), + ALIAS_CAMEL_CASE("AlIaS"), GENERIC_CAMEL_CASE("GeNerIc"), GENERIC("generic"), RUBICON("rubicon"), @@ -20,6 +21,7 @@ enum BidderName { ACUITYADS("acuityads"), AAX("aax"), ADKERNEL("adkernel"), + IX("ix"), GRID("grid"), MEDIANET("medianet") diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy new file mode 100644 index 00000000000..3184fb17fee --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy @@ -0,0 +1,8 @@ +package org.prebid.server.functional.model.bidder + +class GeneralBidderAdapter extends Generic { + + String siteId + List size + String sid +} diff --git a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy index 58017ce1490..c1889dc51bc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy @@ -4,8 +4,8 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.Imp -@ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) class BidderImp extends Imp { BidderImpExt ext diff --git a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy index e0e3b26d02a..d07ca31ad9d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy @@ -1,12 +1,12 @@ package org.prebid.server.functional.model.bidderspecific import groovy.transform.ToString -import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.GeneralBidderAdapter import org.prebid.server.functional.model.request.auction.ImpExt @ToString(includeNames = true, ignoreNulls = true) class BidderImpExt extends ImpExt { - Generic bidder + GeneralBidderAdapter bidder Rp rp } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy new file mode 100644 index 00000000000..baa19a80db4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy @@ -0,0 +1,31 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AbTest { + + Boolean enabled + String moduleCode + @JsonProperty("module_code") + String moduleCodeSnakeCase + Set accounts + Integer percentActive + @JsonProperty("percent_active") + Integer percentActiveSnakeCase + Boolean logAnalyticsTag + @JsonProperty("log_analytics_tag") + Boolean logAnalyticsTagSnakeCase + + static AbTest getDefault(String moduleCode, List accounts = null) { + new AbTest(enabled: true, + moduleCode: moduleCode, + accounts: accounts, + percentActive: 0, + logAnalyticsTag: true) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy index 017058e0b90..0a15cead562 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy @@ -11,6 +11,7 @@ class AccountAnalyticsConfig { Map auctionEvents Boolean allowClientDetails + AnalyticsModule modules @JsonProperty("auction_events") Map auctionEventsSnakeCase diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 63d27073805..4e423c03312 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.request.auction.BidAdjustment import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.MediaType @@ -12,7 +13,7 @@ import org.prebid.server.functional.model.response.auction.MediaType @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountAuctionConfig { - String priceGranularity + PriceGranularityType priceGranularity Integer bannerCacheTtl Integer videoCacheTtl Integer truncateTargetAttr @@ -26,9 +27,11 @@ class AccountAuctionConfig { Map preferredMediaType @JsonProperty("privacysandbox") PrivacySandbox privacySandbox + @JsonProperty("bidadjustments") + BidAdjustment bidAdjustments @JsonProperty("price_granularity") - String priceGranularitySnakeCase + PriceGranularityType priceGranularitySnakeCase @JsonProperty("banner_cache_ttl") Integer bannerCacheTtlSnakeCase @JsonProperty("video_cache_ttl") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy index 24f3ab97d77..bab4ec983a3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy @@ -13,4 +13,5 @@ class AccountHooksConfiguration { @JsonProperty("execution_plan") ExecutionPlan executionPlanSnakeCase PbsModulesConfig modules + AdminConfig admin } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy new file mode 100644 index 00000000000..755a47bcbaa --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.ModuleName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AdminConfig { + + Map moduleExecution +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy new file mode 100644 index 00000000000..5b53fedbe8d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AnalyticsModule { + + LogAnalytics logAnalytics +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy new file mode 100644 index 00000000000..6486e292ed5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AppVideoHtml { + + Boolean enabled + List excludedBidders +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy index b73f4fcaeb3..9ded40849d0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy @@ -12,4 +12,16 @@ class EndpointExecutionPlan { new EndpointExecutionPlan(stages: stages.collectEntries { it -> [(it): StageExecutionPlan.getModuleStageExecutionPlan(name, it)] } as Map) } + + static EndpointExecutionPlan getModulesEndpointExecutionPlan(Map> modulesStages) { + new EndpointExecutionPlan( + stages: modulesStages.collectEntries { stage, moduleNames -> + [(stage): new StageExecutionPlan( + groups: moduleNames.collect { moduleName -> + ExecutionGroup.getModuleExecutionGroup(moduleName, stage) + } + )] + } as Map + ) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy index 766139bc5c5..653f8c8cbea 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy @@ -1,14 +1,22 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.ModuleName @ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class ExecutionPlan { + List abTests Map endpoints static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, ModuleName moduleName, List stage) { new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModuleEndpointExecutionPlan(moduleName, stage)]) } + + static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, Map> modulesStages) { + new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModulesEndpointExecutionPlan(modulesStages)]) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy b/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy new file mode 100644 index 00000000000..cc6d9cc033a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class LogAnalytics { + + Boolean enabled + String additionalData +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 11173093f85..247bdea4353 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -7,8 +7,10 @@ import org.prebid.server.functional.model.ModuleName enum ModuleHookImplementation { PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"), + RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses"), ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), - ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") + ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response"), + PB_REQUEST_CORRECTION_PROCESSED_AUCTION_REQUEST("pb-request-correction-processed-auction-request"), @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy new file mode 100644 index 00000000000..de8eae06d04 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy @@ -0,0 +1,76 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingActionOverride { + + List enforceBlocks + List blockedAdomain + List blockedApp + List blockedBannerAttr + List blockedVideoAttr + List blockedAudioAttr + List blockedAdvCat + List blockedBannerType + + List blockUnknownAdomain + List blockUnknownAdvCat + + List allowedAdomainForDeals + List allowedAppForDeals + List allowedBannerAttrForDeals + List allowedVideoAttrForDeals + List allowedAudioAttrForDeals + List allowedAdvCatForDeals + + static Ortb2BlockingActionOverride getDefaultOverride(Ortb2BlockingAttribute attribute, + List blocked, + List allowedForDeals = null) { + + new Ortb2BlockingActionOverride().tap { + switch (attribute) { + case BADV: + blockedAdomain = blocked + allowedAdomainForDeals = allowedForDeals + break + case BAPP: + blockedApp = blocked + allowedAppForDeals = allowedForDeals + break + case BANNER_BATTR: + blockedBannerAttr = blocked + allowedBannerAttrForDeals = allowedForDeals + break + case VIDEO_BATTR: + blockedVideoAttr = blocked + allowedVideoAttrForDeals = allowedForDeals + break + case AUDIO_BATTR: + blockedAudioAttr = blocked + allowedAudioAttrForDeals = allowedForDeals + break + case BCAT: + blockedAdvCat = blocked + allowedAdvCatForDeals = allowedForDeals + break + case BTYPE: + blockedBannerType = blocked + break + default: + throw new IllegalArgumentException("Unknown attribute type: $attribute") + } + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy new file mode 100644 index 00000000000..15c54c2c021 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum Ortb2BlockingAttribute { + + BADV('badv'), + BAPP('bapp'), + BANNER_BATTR('battr'), + VIDEO_BATTR('battr'), + AUDIO_BATTR('battr'), + BCAT('bcat'), + BTYPE('btype') + + @JsonValue + final String value + + Ortb2BlockingAttribute(String value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy new file mode 100644 index 00000000000..9e622472024 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy @@ -0,0 +1,78 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingAttributeConfig { + + Boolean enforceBlocks + + Object blockedAdomain + Object blockedApp + Object blockedBannerAttr + Object blockedVideoAttr + Object blockedAudioAttr + Object blockedAdvCat + Object blockedBannerType + + Object blockUnknownAdomain + Object blockUnknownAdvCat + + Object allowedAdomainForDeals + Object allowedAppForDeals + Object allowedBannerAttrForDeals + Object allowedVideoAttrForDeals + Object allowedAudioAttrForDeals + Object allowedAdvCatForDeals + + Ortb2BlockingActionOverride actionOverrides + + static getDefaultConfig(Object ortb2Attributes, Ortb2BlockingAttribute attributeName, Object ortb2AttributesForDeals = null) { + new Ortb2BlockingAttributeConfig().tap { + enforceBlocks = false + switch (attributeName) { + case BADV: + blockedAdomain = ortb2Attributes + allowedAdomainForDeals = ortb2AttributesForDeals + break + case BAPP: + blockedApp = ortb2Attributes + allowedAppForDeals = ortb2AttributesForDeals + break + case BANNER_BATTR: + blockedBannerAttr = ortb2Attributes + allowedBannerAttrForDeals = ortb2AttributesForDeals + break + case VIDEO_BATTR: + blockedVideoAttr = ortb2Attributes + allowedVideoAttrForDeals = ortb2AttributesForDeals + break + case AUDIO_BATTR: + blockedAudioAttr = ortb2Attributes + allowedAudioAttrForDeals = ortb2AttributesForDeals + break + case BCAT: + blockedAdvCat = ortb2Attributes + allowedAdvCatForDeals = ortb2AttributesForDeals + break + case BTYPE: + blockedBannerType = ortb2Attributes + break + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } + } + +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy new file mode 100644 index 00000000000..6e983374577 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.response.auction.MediaType + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingConditions { + + List bidders + List mediaType + List dealIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy new file mode 100644 index 00000000000..1cef82cbe52 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Ortb2BlockingConfig { + + Map attributes +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy new file mode 100644 index 00000000000..987aa11e421 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingOverride { + + Object override + Ortb2BlockingConditions conditions +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy new file mode 100644 index 00000000000..5d7a980115b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy @@ -0,0 +1,29 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class PbRequestCorrectionConfig { + + @JsonProperty("pbsdkAndroidInstlRemove") + Boolean interstitialCorrectionEnabled + @JsonProperty("pbsdkUaCleanup") + Boolean userAgentCorrectionEnabled + @JsonProperty("pbsdk-android-instl-remove") + Boolean interstitialCorrectionEnabledKebabCase + @JsonProperty("pbsdk-ua-cleanup") + Boolean userAgentCorrectionEnabledKebabCase + + Boolean enabled + + static PbRequestCorrectionConfig getDefaultConfigWithInterstitial(Boolean interstitialCorrectionEnabled = true, + Boolean enabled = true) { + new PbRequestCorrectionConfig(enabled: enabled, interstitialCorrectionEnabled: interstitialCorrectionEnabled) + } + + static PbRequestCorrectionConfig getDefaultConfigWithUserAgentCorrection(Boolean userAgentCorrectionEnabled = true, + Boolean enabled = true) { + new PbRequestCorrectionConfig(enabled: enabled, userAgentCorrectionEnabled: userAgentCorrectionEnabled) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy new file mode 100644 index 00000000000..46af75deac6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class PbResponseCorrection { + + Boolean enabled + AppVideoHtml appVideoHtml +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 33b2e1478ad..59f640f966c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -10,4 +10,7 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter class PbsModulesConfig { RichmediaFilter pbRichmediaFilter + Ortb2BlockingConfig ortb2Blocking + PbResponseCorrection pbResponseCorrection + PbRequestCorrectionConfig pbRequestCorrection } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy new file mode 100644 index 00000000000..957a2d880bf --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.Range + +enum PriceGranularityType { + + LOW(2, [Range.getDefault(5, 0.5)]), + MEDIUM(2, [Range.getDefault(20, 0.1)]), + MED(2, [Range.getDefault(20, 0.1)]), + HIGH(2, [Range.getDefault(20, 0.01)]), + AUTO(2, [Range.getDefault(5, 0.05), Range.getDefault(10, 0.1), Range.getDefault(20, 0.5)]), + DENSE(2, [Range.getDefault(3, 0.01), Range.getDefault(8, 0.05), Range.getDefault(20, 0.5)]), + UNKNOWN(null, []) + + final Integer precision + final List ranges + + PriceGranularityType(Integer precision, List ranges) { + this.precision = precision + this.ranges = ranges + } + + @JsonValue + String toLowerCase() { + return name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy index c77fd9ebcda..178f22552ae 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy @@ -6,20 +6,22 @@ import groovy.transform.ToString @ToString enum Stage { - ENTRYPOINT("entrypoint"), - RAW_AUCTION_REQUEST("raw-auction-request"), - PROCESSED_AUCTION_REQUEST("processed-auction-request"), - BIDDER_REQUEST("bidder-request"), - RAW_BIDDER_RESPONSE("raw-bidder-response"), - PROCESSED_BIDDER_RESPONSE("processed-bidder-response"), - ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses"), - AUCTION_RESPONSE("auction-response") + ENTRYPOINT("entrypoint", "entrypoint"), + RAW_AUCTION_REQUEST("raw-auction-request", "rawauction"), + PROCESSED_AUCTION_REQUEST("processed-auction-request", "procauction"), + BIDDER_REQUEST("bidder-request", "bidrequest"), + RAW_BIDDER_RESPONSE("raw-bidder-response", "rawbidresponse"), + PROCESSED_BIDDER_RESPONSE("processed-bidder-response", "procbidresponse"), + ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses", "allprocbidresponses"), + AUCTION_RESPONSE("auction-response", "auctionresponse") @JsonValue final String value + final String metricValue - Stage(String value) { + Stage(String value, String metricValue) { this.value = value + this.metricValue = metricValue } @Override diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy index 431890c371d..a9d913eccd1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy @@ -16,6 +16,7 @@ class PriceFloorData implements ResponseModel { String floorProvider Currency currency Integer skipRate + Integer useFetchDataRate String floorsSchemaVersion Integer modelTimestamp List modelGroups diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy new file mode 100644 index 00000000000..953f66fd988 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.Currency + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class AdjustmentRule { + + @JsonProperty('adjtype') + AdjustmentType adjustmentType + BigDecimal value + Currency currency +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy new file mode 100644 index 00000000000..20574d525a1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum AdjustmentType { + + MULTIPLIER, CPM, STATIC, UNKNOWN + + @JsonValue + String getValue() { + name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy index b31926c14b5..ee3c1c9a8f0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy @@ -6,4 +6,5 @@ import groovy.transform.ToString class AppExt { AppExtData data + AppPrebid prebid } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy new file mode 100644 index 00000000000..edb365d4d6f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AppPrebid { + + String source + String version +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy new file mode 100644 index 00000000000..7f7250a6a75 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class BidAdjustment { + + Map mediaType + Integer version + + static getDefaultWithSingleMediaTypeRule(BidAdjustmentMediaType type, + BidAdjustmentRule rule, + Integer version = PBSUtils.randomNumber) { + new BidAdjustment(mediaType: [(type): rule], version: version) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy index a005d407241..9cb90edb27b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy @@ -15,7 +15,6 @@ class BidAdjustmentFactors { Map adjustments Map> mediaTypes - @JsonAnyGetter Map getAdjustments() { adjustments diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy index a959f5b800c..26a58655215 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy @@ -8,7 +8,10 @@ enum BidAdjustmentMediaType { AUDIO("audio"), NATIVE("native"), VIDEO("video"), - VIDEO_OUTSTREAM("video-outstream") + VIDEO_IN_STREAM("video-instream"), + VIDEO_OUT_STREAM("video-outstream"), + ANY('*'), + UNKNOWN('unknown') @JsonValue String value diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy new file mode 100644 index 00000000000..4fcfc1125e1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class BidAdjustmentRule { + + @JsonProperty('*') + Map> wildcardBidder + Map> generic + Map> alias +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index 1157580209a..aa9da45a4b6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -5,10 +5,12 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO @EqualsAndHashCode @@ -22,7 +24,7 @@ class BidRequest { Dooh dooh Device device User user - Integer test + DebugCondition test Integer at Long tmax List wseat @@ -47,6 +49,10 @@ class BidRequest { getDefaultRequest(channel, Imp.getDefaultImpression(VIDEO)) } + static BidRequest getDefaultNativeRequest(DistributionChannel channel = SITE) { + getDefaultRequest(channel, Imp.getDefaultImpression(NATIVE)) + } + static BidRequest getDefaultAudioRequest(DistributionChannel channel = SITE) { getDefaultRequest(channel, Imp.getDefaultImpression(AUDIO)) } @@ -63,7 +69,7 @@ class BidRequest { regs = Regs.defaultRegs id = UUID.randomUUID() tmax = 2500 - ext = new BidRequestExt(prebid: new Prebid(debug: 1)) + ext = new BidRequestExt(prebid: new Prebid(debug: ENABLED)) if (channel == SITE) { site = Site.defaultSite } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy index f7aee45fb75..c253291dbf8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy @@ -11,4 +11,5 @@ class BidRequestExt { AppNexus appnexus String bc String platform + IxDiag ixdiag } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy index 9b5c78f5d97..a1078731f44 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy @@ -19,6 +19,7 @@ class Bidder { @JsonProperty("appnexus") AppNexus appNexus Openx openx + Ix ix static Bidder getDefaultBidder() { new Bidder().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy index dc01fb12b3a..ee029b56c5b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy @@ -1,9 +1,12 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class BidderControls { GenericPreferredBidder generic + @JsonProperty("GeNeRiC") + GenericPreferredBidder genericAnyCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy new file mode 100644 index 00000000000..aa7bd511cb2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class ConsentedProvidersSettings { + + String consentedProviders +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy index 9895f860b40..6dbe31760f4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy @@ -2,8 +2,11 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class Deal { @@ -15,4 +18,8 @@ class Deal { List wseat List wadomain DealExt ext -} + + static Deal getDefaultDeal() { + new Deal(id: PBSUtils.randomString) + } + } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy index 90b57aa8fb9..d59c145551a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy @@ -1,7 +1,9 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true) class DealExt { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy index 4ab7823193c..42b54c6522a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy @@ -2,8 +2,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class DealLineItem { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy new file mode 100644 index 00000000000..066080c56da --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum DebugCondition { + + DISABLED(0), ENABLED(1) + + @JsonValue + final int value + + private DebugCondition(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy index 1b70bd08799..2cb344f6967 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @@ -10,11 +11,18 @@ class Eid { String source List uids + String inserter + String matcher + @JsonProperty("mm") + Integer matchMethod static Eid getDefaultEid(String source = PBSUtils.randomString) { new Eid().tap { it.source = source it.uids = [Uid.defaultUid] + it.inserter = PBSUtils.randomString + it.matcher = PBSUtils.randomString + it.matchMethod = PBSUtils.randomNumber } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy index c669d61f5a0..6eec49cf39d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy @@ -6,7 +6,7 @@ import groovy.transform.ToString @ToString enum FetchStatus { - NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK + NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK, SKIPPED, RUN @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy index 3508dfa60fe..f6d1798ca57 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class Format { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index 0e6195c977b..13c97a36ba4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -30,12 +30,12 @@ class Imp { Pmp pmp String displayManager String displayManagerVer - Integer instl + OperationState instl String tagId BigDecimal bidFloor Currency bidFloorCur Integer clickBrowser - Integer secure + SecurityLevel secure List iframeBuster Integer rwdd Integer ssai @@ -90,7 +90,8 @@ class Imp { (bidder.genericCamelCase): BidderName.GENERIC_CAMEL_CASE, (bidder.rubicon) : BidderName.RUBICON, (bidder.appNexus) : BidderName.APPNEXUS, - (bidder.openx) : BidderName.OPENX + (bidder.openx) : BidderName.OPENX, + (bidder.ix) : BidderName.IX ].findAll { it.key } if (bidderNames.size() != 1) { @@ -99,4 +100,14 @@ class Imp { bidderNames.values().first() } + + @JsonIgnore + List getMediaTypes() { + return [ + (banner ? BANNER : null), + (video ? VIDEO : null), + (nativeObj ? NATIVE : null), + (audio ? AUDIO : null) + ].findAll { it } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy index fe8cd0f089c..e817a4540a0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy @@ -22,6 +22,7 @@ class ImpExt { ImpExtContextData data String tid String gpid + String sid Integer ae String all String skadn diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy index 21782323974..fc771e59919 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @@ -17,6 +18,7 @@ class ImpExtPrebid { Bidder bidder ImpExtPrebidFloors floors Map passThrough + Map imp static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy new file mode 100644 index 00000000000..a620c7646b1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.EqualsAndHashCode +import org.prebid.server.functional.util.PBSUtils + +@EqualsAndHashCode +class Ix { + + String siteId + List size + String sid + + static Ix getDefault() { + new Ix().tap { + siteId = PBSUtils.randomString + size = [PBSUtils.randomNumber, PBSUtils.randomNumber] + sid = PBSUtils.randomString + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy new file mode 100644 index 00000000000..7bc0adc1054 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class IxDiag { + + String pbsv + String pbjsv + String multipleSiteIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy index ce72554490b..15fb3a6d464 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy @@ -2,12 +2,18 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class Pmp { Integer privateAuction List deals + + static Pmp getDefaultPmp() { + new Pmp(deals: [Deal.defaultDeal]) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index 7240fd91719..ba89f5680fa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -10,11 +10,12 @@ import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) class Prebid { - Integer debug + DebugCondition debug Boolean returnAllBidStatus Map aliases Map aliasgvlids BidAdjustmentFactors bidAdjustmentFactors + BidAdjustment bidAdjustments PrebidCurrency currency Targeting targeting TraceLevel trace diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy index 6aa25569030..f6ab5331ca7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy @@ -1,12 +1,11 @@ package org.prebid.server.functional.model.request.auction -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.config.LogAnalytics -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class PrebidAnalytics { AnalyticsOptions options + LogAnalytics logAnalytics } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy index 29f4472cab2..873c686a578 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy @@ -1,10 +1,21 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.config.PriceGranularityType @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class PriceGranularity { Integer precision List ranges + + static PriceGranularity getDefault(PriceGranularityType granularity) { + new PriceGranularity(precision: granularity.precision, ranges: granularity.ranges) + } + + static PriceGranularity getDefault() { + getDefault(PriceGranularityType.MED) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy index 59fb0b34c25..9bdb94f007d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy @@ -4,7 +4,8 @@ enum PublicCountryIp { USA_IP("209.232.44.21", "d646:2414:17b2:f371:9b62:f176:b4c0:51cd"), UKR_IP("193.238.111.14", "3080:f30f:e4bc:0f56:41be:6aab:9d0a:58e2"), - CAN_IP("70.71.245.39", "f9b2:c742:1922:7d4b:7122:c7fc:8b75:98c8") + CAN_IP("70.71.245.39", "f9b2:c742:1922:7d4b:7122:c7fc:8b75:98c8"), + BGR_IP("31.211.128.0", "2002:1fd3:8000:0000:0000:0000:0000:0000") final String v4 final String v6 diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy index c5fa8cb2220..1b106b67faa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy @@ -1,10 +1,16 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Range { BigDecimal max BigDecimal increment + + static Range getDefault(Integer max, BigDecimal increment) { + new Range(max: max, increment: increment) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy index 1d8aa6f077d..6e647c16817 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy @@ -17,7 +17,7 @@ class Regs { static Regs getDefaultRegs() { new Regs().tap { - ext = new RegsExt(gdpr: 0) + gdpr = 0 } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy index f235dfbd600..d7e9cb2242e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy @@ -8,7 +8,10 @@ import groovy.transform.ToString @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class RegsExt { + @Deprecated(since = "enabling support of ortb 2.6") Integer gdpr + Integer coppa + @Deprecated(since = "enabling support of ortb 2.6") String usPrivacy String gpc Dsa dsa diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy new file mode 100644 index 00000000000..8ca88307215 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum SecurityLevel { + + NON_SECURE(0), SECURE(1) + + @JsonValue + private final Integer level + + SecurityLevel(int level) { + this.level = level + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy index 2cab8a9fdc1..cde5f2268de 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy @@ -10,4 +10,6 @@ class StoredAuctionResponse { String id @JsonProperty("seatbidarr") List seatBids + @JsonProperty("seatbidobj") + SeatBid seatBidObject } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy index e547d8f37ed..af07e197c28 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy @@ -1,8 +1,12 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class UserExt { String consent @@ -11,6 +15,9 @@ class UserExt { UserTime time UserExtData data UserExtPrebid prebid + ConsentedProvidersSettings consentedProvidersSettings + @JsonProperty("ConsentedProvidersSettings") + ConsentedProvidersSettings consentedProvidersSettingsCamelCase static UserExt getFPDUserExt() { new UserExt(data: UserExtData.FPDUserExtData) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index bc2ef7f5a5c..a70ee05eac3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -43,6 +43,8 @@ class Video { List companionad List api List companiontype + @JsonProperty("poddedupe") + List podDeduplication static Video getDefaultVideo() { new Video(mimes: ["video/mp4"], weight: 300, height: 200) diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index e4fb04de375..22e29b76908 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -47,7 +47,8 @@ class Bid implements ObjectMapperWrapper { Integer heightRatio Integer exp Integer dur - Integer mtype + @JsonProperty("mtype") + BidMediaType mediaType Integer slotinpod BidExt ext diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy new file mode 100644 index 00000000000..76aa2a558f9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum BidMediaType { + + BANNER(1), + VIDEO(2), + AUDIO(3), + NATIVE(4) + + @JsonValue + final Integer value + + BidMediaType(Integer value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index 6cc4fc5605a..2fb7d75bbf7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy @@ -4,15 +4,26 @@ import com.fasterxml.jackson.annotation.JsonValue enum BidRejectionReason { - NO_BID(0), - TIMED_OUT(101), - REJECTED_BY_HOOK(200), - REJECTED_BY_PRIVACY(202), - REJECTED_BY_MEDIA_TYPE(204), - GENERAL(300), - REJECTED_DUE_TO_PRICE_FLOOR(301), - REJECTED_DUE_TO_DSA(305), - OTHER_ERROR(100) + ERROR_NO_BID(0), + ERROR_GENERAL(100), + ERROR_TIMED_OUT(101), + ERROR_INVALID_BID_RESPONSE(102), + ERROR_BIDDER_UNREACHABLE(103), + ERROR_REQUEST(104), + + REQUEST_BLOCKED_GENERAL(200), + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + REQUEST_BLOCKED_PRIVACY(204), + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), + + RESPONSE_REJECTED_GENERAL(300), + RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR(301), + RESPONSE_REJECTED_DUE_TO_DSA(305), + RESPONSE_REJECTED_INVALID_CREATIVE(350), + RESPONSE_REJECTED_INVALID_CREATIVE_SIZE(351), + RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE(352), + RESPONSE_REJECTED_ADVERTISER_BLOCKED(356) @JsonValue final Integer code diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy index e62b548fbe6..267a23cc067 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy @@ -13,6 +13,7 @@ enum ErrorType { CACHE("cache"), ALIAS("alias"), TARGETING("targeting"), + IX("ix"), OPENX("openx") @JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy index 32ce2fa727d..c0504a64cc4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy @@ -9,4 +9,6 @@ import groovy.transform.ToString class ExtModule { ModuleTrace trace + ModuleError errors + ModuleWarning warnings } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy index 3f1594380f4..c5c1a828f98 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy @@ -14,4 +14,5 @@ class InvocationResult { ResponseAction action HookId hookId AnalyticsPrebidTag analyticsTags + String message } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy index 257b6287fcf..77c7ffd5ef8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy @@ -6,7 +6,7 @@ import groovy.transform.ToString @ToString enum InvocationStatus { - SUCCESS, FAILURE + SUCCESS, FAILURE, INVOCATION_FAILURE @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy index f3e376a30dd..5e46ef8425a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy @@ -8,12 +8,15 @@ enum MediaType { VIDEO, AUDIO, NATIVE, + WILDCARD, NULL @JsonValue String getValue() { if (name() == "NULL") { return null + } else if (name() == "WILDCARD") { + return "*" } name().toLowerCase() } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy index 3942b170875..8711bd395c6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy @@ -5,7 +5,8 @@ import com.fasterxml.jackson.annotation.JsonValue enum ModuleActivityName { ORTB2_BLOCKING('enforce-blocking'), - REJECT_RICHMEDIA('reject-richmedia') + REJECT_RICHMEDIA('reject-richmedia'), + AB_TESTING('core-module-abtests') @JsonValue final String value diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy new file mode 100644 index 00000000000..138b5e40507 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class ModuleError { + + Map> ortb2Blocking +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy index e76e8fb3f54..9a1e9d1b440 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy @@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.ModuleName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) @EqualsAndHashCode class ModuleValue { + ModuleName module String richmediaFormat } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy new file mode 100644 index 00000000000..5c6d4ebed44 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class ModuleWarning { + + Map> ortb2Blocking +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy index 1a786670ba8..1bce783d048 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum ResponseAction { - UPDATE, NO_ACTION + UPDATE, NO_ACTION, NO_INVOCATION @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy index f1a72a9e266..0f155bf55a1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy @@ -3,13 +3,12 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString -import org.prebid.server.functional.model.config.Stage @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class TraceOutcome { - Stage entity + String entity Long executionTimeMillis List groups } diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index fd0bcac255c..bac2badd46a 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -379,6 +379,21 @@ class PrebidServerService implements ObjectMapperWrapper { filteredLogs } + String getLogsByValue(String value) { + if (!value) { + throw new IllegalArgumentException("Value is null or empty") + } + getPbsLogsByValue(value) + } + + Boolean isContainLogsByValue(String value) { + getPbsLogsByValue(value) != null + } + + private String getPbsLogsByValue(String value) { + pbsContainer.logs.split("\n").find { it.contains(value) } + } + T getValueFromContainer(String path, Class clazz) { pbsContainer.copyFileFromContainer(path, { inputStream -> return decode(inputStream, clazz) diff --git a/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy new file mode 100644 index 00000000000..4a25b6d6ca0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy @@ -0,0 +1,103 @@ +package org.prebid.server.functional.service + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.localstack.LocalStackContainer +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.CreateBucketRequest +import software.amazon.awssdk.services.s3.model.DeleteBucketRequest +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import software.amazon.awssdk.services.s3.model.PutObjectResponse + +final class S3Service implements ObjectMapperWrapper { + + private final S3Client s3PbsService + private final LocalStackContainer localStackContainer + + static final def DEFAULT_ACCOUNT_DIR = 'account' + static final def DEFAULT_IMPS_DIR = 'stored-impressions' + static final def DEFAULT_REQUEST_DIR = 'stored-requests' + static final def DEFAULT_RESPONSE_DIR = 'stored-responses' + + S3Service(LocalStackContainer localStackContainer) { + this.localStackContainer = localStackContainer + s3PbsService = S3Client.builder() + .endpointOverride(localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create( + localStackContainer.getAccessKey(), + localStackContainer.getSecretKey()))) + .region(Region.of(localStackContainer.getRegion())) + .build() + } + + String getAccessKeyId() { + localStackContainer.accessKey + } + + String getSecretKeyId() { + localStackContainer.secretKey + } + + String getEndpoint() { + "http://${localStackContainer.getNetworkAliases().get(0)}:${localStackContainer.getExposedPorts().get(0)}" + } + + String getRegion() { + localStackContainer.region + } + + void createBucket(String bucketName) { + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .build() + s3PbsService.createBucket(createBucketRequest) + } + + void deleteBucket(String bucketName) { + DeleteBucketRequest deleteBucketRequest = DeleteBucketRequest.builder() + .bucket(bucketName) + .build() + s3PbsService.deleteBucket(deleteBucketRequest) + } + + void purgeBucketFiles(String bucketName) { + s3PbsService.listObjectsV2(ListObjectsV2Request.builder().bucket(bucketName).build()).contents().each { files -> + s3PbsService.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(files.key()).build()) + } + } + + PutObjectResponse uploadAccount(String bucketName, AccountConfig account, String fileName = account.id) { + uploadFile(bucketName, encode(account), "${DEFAULT_ACCOUNT_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredRequest(String bucketName, StoredRequest storedRequest, String fileName = storedRequest.requestId) { + uploadFile(bucketName, encode(storedRequest.requestData), "${DEFAULT_REQUEST_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredResponse(String bucketName, StoredResponse storedRequest, String fileName = storedRequest.responseId) { + uploadFile(bucketName, encode(storedRequest.storedAuctionResponse), "${DEFAULT_RESPONSE_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredImp(String bucketName, StoredImp storedImp, String fileName = storedImp.impId) { + uploadFile(bucketName, encode(storedImp.impData), "${DEFAULT_IMPS_DIR}/${fileName}.json") + } + + PutObjectResponse uploadFile(String bucketName, String fileBody, String path) { + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(path) + .build() + s3PbsService.putObject(putObjectRequest, RequestBody.fromString(fileBody)) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy index 53cbecf2289..70c99a2a833 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy @@ -4,10 +4,13 @@ import org.prebid.server.functional.testcontainers.container.NetworkServiceConta import org.prebid.server.functional.util.SystemProperties import org.testcontainers.containers.MySQLContainer import org.testcontainers.containers.Network +import org.testcontainers.containers.localstack.LocalStackContainer import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.lifecycle.Startables +import org.testcontainers.utility.DockerImageName import static org.prebid.server.functional.util.SystemProperties.MOCKSERVER_VERSION +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3 class Dependencies { @@ -34,17 +37,21 @@ class Dependencies { static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION) .withNetwork(network) + static LocalStackContainer localStackContainer + static void start() { if (IS_LAUNCH_CONTAINERS) { - Startables.deepStart([networkServiceContainer, mysqlContainer]) - .join() + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) + .withNetwork(network) + .withServices(S3) + Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]).join() } } static void stop() { if (IS_LAUNCH_CONTAINERS) { - [networkServiceContainer, mysqlContainer].parallelStream() - .forEach({ it.stop() }) + [networkServiceContainer, mysqlContainer, localStackContainer].parallelStream() + .forEach({ it.stop() }) } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 7598fd4cfae..aa10ebaf7a4 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -127,8 +127,6 @@ LIMIT 1 "adapters.generic.aliases.nativo.meta-info.site-media-types" : "", "adapters.generic.aliases.infytv.meta-info.app-media-types" : "", "adapters.generic.aliases.infytv.meta-info.site-media-types" : "", - "adapters.generic.aliases.loopme.meta-info.app-media-types" : "", - "adapters.generic.aliases.loopme.meta-info.site-media-types" : "", "adapters.generic.aliases.zeta-global-ssp.meta-info.app-media-types" : "", "adapters.generic.aliases.zeta-global-ssp.meta-info.site-media-types": "", "adapters.generic.aliases.ccx.meta-info.app-media-types" : "", diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy index a0c5631aac6..e0911a2b1ca 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy @@ -64,6 +64,6 @@ class PbsServiceFactory { private static int getMaxContainerCount() { USE_FIXED_CONTAINER_PORTS ? 1 - : SystemProperties.getPropertyOrDefault("tests.max-container-count", 2) + : SystemProperties.getPropertyOrDefault("tests.max-container-count", 5) } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index 5629826942b..b3f938a7ca0 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -91,7 +91,6 @@ class PrebidServerContainer extends GenericContainer { private static String normalizeProperty(String property) { property.replace(".", "_") - .replace("-", "") .replace("[", "_") .replace("]", "_") } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy index 1c47147f596..224f7c8b228 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy @@ -52,6 +52,10 @@ class PrebidCache extends NetworkScaffolding { .collect { decode(it.body.toString(), BidCacheRequest) } } + Map> getRequestHeaders(String impId) { + getLastRecordedRequestHeaders(getRequest(impId)) + } + @Override HttpRequest getRequest() { request().withMethod("POST") diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy index 3f84faa7c44..343a118f53a 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy @@ -10,6 +10,8 @@ import org.testcontainers.containers.MockServerContainer import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V2 +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.Vendor import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID @@ -46,6 +48,7 @@ class VendorList extends NetworkScaffolding { def prepareEncodeResponseBody = encode(defaultVendorListResponse.tap { it.tcfPolicyVersion = tcfPolicyVersion.vendorListVersion it.vendors = vendors + it.gvlSpecificationVersion = tcfPolicyVersion >= TcfPolicyVersion.TCF_POLICY_V4 ? V3 : V2 }) mockServerClient.when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy index 9205542bbb8..96a78df89a8 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -4,9 +4,12 @@ import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.ConsentedProvidersSettings import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils @@ -56,7 +59,7 @@ class AmpSpec extends BaseSpec { assert exception.responseBody == "Invalid request format: request.${channel.value.toLowerCase()} must not exist in AMP stored requests." where: - channel << [DistributionChannel.APP, DistributionChannel.DOOH] + channel << [DistributionChannel.APP, DistributionChannel.DOOH] } def "PBS should return info from the stored response when it's defined in the stored request"() { @@ -107,7 +110,7 @@ class AmpSpec extends BaseSpec { and: "Default stored request with specified: gdpr, debug" def ampStoredRequest = BidRequest.defaultStoredRequest - ampStoredRequest.regs.ext.gdpr = 1 + ampStoredRequest.regs.gdpr = 1 and: "Stored request in DB" def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) @@ -178,6 +181,81 @@ class AmpSpec extends BaseSpec { assert !bidderRequest.imp[0]?.tagId assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight - assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.ext.gdpr + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr + } + + def "PBS should pass addtl_consent to user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers"() { + given: "Default amp request with addtlConsent" + def randomAddtlConsent = PBSUtils.randomString + def ampRequest = AmpRequest.defaultAmpRequest.tap { + addtlConsent = randomAddtlConsent + } + + and: "Save storedRequest into DB" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt( + consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: PBSUtils.randomString), + consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: PBSUtils.randomString))) + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain addtl consent" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders == randomAddtlConsent + assert bidderRequest.user.ext.consentedProvidersSettings.consentedProviders == randomAddtlConsent + } + + def "PBS should process original user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers when ampRequest doesn't contain addtl_consent"() { + given: "Default amp request with addtlConsent" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + addtlConsent = null + } + + and: "Save storedRequest into DB" + def consentProvidersKebabCase = PBSUtils.randomString + def consentProviders = PBSUtils.randomString + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt( + consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: consentProvidersKebabCase), + consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: consentProviders))) + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain requested consent" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders == consentProvidersKebabCase + assert bidderRequest.user.ext.consentedProvidersSettings.consentedProviders == consentProviders + } + + def "PBS should left user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers empty when addtl_consent and original fields are empty"() { + given: "Default amp request with addtlConsent" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + addtlConsent = null + } + + and: "Save storedRequest into DB" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt( + consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: null), + consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: null))) + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request shouldn't contain consent" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert !bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders + assert !bidderRequest.user.ext.consentedProvidersSettings.consentedProviders } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy index c3b6f9e76b0..b113f649834 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy @@ -1,7 +1,13 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.config.AccountAnalyticsConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AnalyticsModule +import org.prebid.server.functional.model.config.LogAnalytics +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.mock.services.pubstack.PubStackResponse import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.PrebidAnalytics import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.Dependencies import org.prebid.server.functional.testcontainers.PbsConfig @@ -13,7 +19,15 @@ import spock.lang.Shared class AnalyticsSpec extends BaseSpec { private static final String SCOPE_ID = UUID.randomUUID() + private static final Map ENABLED_DEBUG_LOG_MODE = ["logging.level.root": "debug"] private static final PrebidServerService pbsService = pbsServiceFactory.getService(PbsConfig.getPubstackAnalyticsConfig(SCOPE_ID)) + private static final PrebidServerService pbsServiceWithLogAnalytics = pbsServiceFactory.getService( + ENABLED_DEBUG_LOG_MODE + ['analytics.log.enabled' : 'true', + 'analytics.global.adapters': 'logAnalytics']) + private static final PrebidServerService pbsServiceWithoutLogAnalytics = pbsServiceFactory.getService( + ENABLED_DEBUG_LOG_MODE + ['analytics.log.enabled' : 'true', + 'analytics.global.adapters': '']) + @Shared PubStackAnalytics analytics = new PubStackAnalytics(Dependencies.networkServiceContainer).tap { @@ -34,4 +48,216 @@ class AnalyticsSpec extends BaseSpec { then: "PBS should call pubstack analytics" PBSUtils.waitUntil { analytics.requestCount == analyticsRequestCount + 1 } } + + def "PBS should populate log analytics when logging enabled in global config but not in account config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: null)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "Analytics bid request shouldn't be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + } + + def "PBS shouldn't populate log analytics when log analytics is directly non-restricted for account and disabled in global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + then: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS should populate log analytics when log analytics is directly non-restricted for account and enabled global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Analytics bid request shouldn't be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS shouldn't populate log analytics when log analytics is directly restricted for account and enabled in global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: false) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS shouldn't populate log analytics when log disabled in global config and not set for account"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: null)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS should populate log analytics with additional data when log is directly non-restricted for account and data specified"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics() + } + + and: "Account in the DB" + def additionalData = PBSUtils.randomString + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: additionalData) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics.logAnalytics + + then: "Analytics bid request should be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS should populate log analytics with additional data from request when data specified in request only"() { + given: "Basic bid request" + def additionalData = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics(logAnalytics: new LogAnalytics(additionalData: additionalData)) + } + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: null) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Analytics bid request should be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS should prioritize logAnalytics from request when data specified in account and request"() { + given: "Basic bid request" + def bidRequestAdditionalData = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics(logAnalytics: new LogAnalytics(additionalData: bidRequestAdditionalData)) + } + + and: "Account in the DB" + def accountAdditionalData = PBSUtils.randomString + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: accountAdditionalData) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Analytics bid request should be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == bidRequestAdditionalData + + where: + logAnalyticsEnable << [null, true] + } + + private static BidRequest extractResolvedRequestFromLog(String logsByText) { + decode(logsByText.split("resolvedrequest")[1] + .replace(";", "") + .replaceFirst(":", "") + .replaceFirst("\"", ""), BidRequest.class) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 20536435161..79eb960787f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -1,22 +1,75 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse +import org.prebid.server.functional.model.request.auction.AdjustmentRule +import org.prebid.server.functional.model.request.auction.AdjustmentType +import org.prebid.server.functional.model.request.auction.BidAdjustment import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors +import org.prebid.server.functional.model.request.auction.BidAdjustmentRule import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes +import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.util.PBSUtils +import java.math.RoundingMode +import java.time.Instant + import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.RUBICON +import static org.prebid.server.functional.model.request.auction.AdjustmentType.CPM +import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER +import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer +import static org.prebid.server.functional.util.PBSUtils.getRandomDecimal class BidAdjustmentSpec extends BaseSpec { + private static final String WILDCARD = '*' + private static final BigDecimal MIN_ADJUST_VALUE = 0 + private static final BigDecimal MAX_MULTIPLIER_ADJUST_VALUE = 99 + private static final BigDecimal MAX_CPM_ADJUST_VALUE = Integer.MAX_VALUE + private static final BigDecimal MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE + private static final Currency DEFAULT_CURRENCY = USD + private static final int BID_ADJUST_PRECISION = 4 + private static final int PRICE_PRECISION = 3 + private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) + private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) + private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, + (GBP): 0.793776804452961], + (GBP): [(USD): 1.2597999770088517, + (EUR): 1.1495574203931487], + (EUR): [(USD): 1.3429368029739777]] + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { + setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) + } + private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig) + def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment" def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { @@ -28,10 +81,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price * + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * bidAdjustmentFactor where: @@ -40,7 +93,7 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should prefer bid price adjustment based on media type when request has per-media-type bid adjustment factors"() { given: "Default bid request with bid adjustment" - def bidAdjustment = PBSUtils.randomDecimal + def bidAdjustment = randomDecimal def mediaTypeBidAdjustment = bidAdjustmentFactor def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap { @@ -54,10 +107,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price * + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * mediaTypeBidAdjustment where: @@ -66,7 +119,7 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should adjust bid price for bidder only when request contains bid adjustment for corresponding bidder"() { given: "Default bid request with bid adjustment" - def bidAdjustment = PBSUtils.randomDecimal + def bidAdjustment = randomDecimal def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap { adjustments = [(adjustmentBidder): bidAdjustment] @@ -78,10 +131,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should not be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price where: adjustmentBidder << [RUBICON, APPNEXUS] @@ -102,10 +155,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should not be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price where: adjustmentMediaType << [VIDEO, NATIVE] @@ -125,7 +178,7 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS should fail the request" def exception = thrown(PrebidServerException) @@ -133,6 +186,971 @@ class BidAdjustmentSpec extends BaseSpec { assert exception.responseBody.contains("Invalid request format: request.ext.prebid.bidadjustmentfactors.$bidderName.value must be a positive number") where: - bidAdjustmentFactor << [0, PBSUtils.randomNegativeNumber] + bidAdjustmentFactor << [MIN_ADJUST_VALUE, PBSUtils.randomNegativeNumber] + } + + def "PBS should adjust bid price for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain default currency" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder with specific dealId when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def dealId = PBSUtils.randomString + def currency = USD + def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.imp.add(Imp.defaultImpression) + bidRequest.cur = [currency] + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + response.seatbid.first.bid.find { it.dealid == dealId } + assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder when account config has bidAdjustments"() { + given: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def currency = USD + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Account in the DB with bidAdjustments" + def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to request config" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should prioritize exact bid price adjustment for matching bidder when request has exact and general bidAdjustment"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: PBSUtils.randomPrice, currency: currency) + def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: PBSUtils.randomPrice, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType) + def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType) + assert response.seatbid.first.bid.first.price == adjustedBidPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + firstRuleType | secondRuleType + MULTIPLIER | CPM + MULTIPLIER | STATIC + MULTIPLIER | MULTIPLIER + CPM | CPM + CPM | STATIC + CPM | MULTIPLIER + STATIC | CPM + STATIC | STATIC + STATIC | MULTIPLIER + } + + def "PBS should convert CPM currency before adjustment when it different from original response currency"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.randomPrice, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [EUR] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def convertedAdjustment = convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) + def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) + assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, bidRequest.cur.first) + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == bidRequest.cur + } + + def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [EUR] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response with JPY currency" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted and converted to original request cur" + assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidRequest.cur.first) + assert response.cur == bidRequest.cur.first + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == bidRequest.cur + } + + def "PBS should apply bidAdjustments after bidAdjustmentFactors when both are present"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def bidAdjustmentFactorsPrice = PBSUtils.randomPrice + def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice + assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when request has invalid value bidAdjustments config"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + + CPM | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + CPM | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + CPM | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + + STATIC | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + STATIC | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + } + + def "PBS shouldn't adjust bid price for matching bidder when request has different bidder name in bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown mediatype"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentPrice = PBSUtils.randomPrice + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown adjustmentType"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " + + "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS shouldn't adjust bid price for matching bidder when multiplier bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [CPM, STATIC] + } + + private static Map getExternalCurrencyConverterConfig() { + ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, + "currency-converter.external-rates.enabled" : "true", + "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), + "currency-converter.external-rates.default-timeout-ms": "4000", + "currency-converter.external-rates.refresh-period-ms" : "900000"] + } + + private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { + return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) + } + + private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { + def conversionRate + if (fromCurrency == toCurrency) { + conversionRate = 1 + } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { + conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] + } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { + conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + } else { + conversionRate = getCrossConversionRate(fromCurrency, toCurrency) + } + conversionRate + } + + private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { + for (Map rates : DEFAULT_CURRENCY_RATES.values()) { + def fromRate = rates?[fromCurrency] + def toRate = rates?[toCurrency] + + if (fromRate && toRate) { + return toRate / fromRate + } + } + + null + } + + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice * adjustedValue, BID_ADJUST_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice - adjustedValue, BID_ADJUST_PRECISION) + case STATIC: + return adjustedValue + default: + return originalPrice + } + } + + private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + placement = videoPlacementSubtypes + } + } + } + + private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + } + } + } + + private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, + VideoPlacementSubtypes videoPlacementSubtypes) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + placement = videoPlacementSubtypes + } + } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy index cc877f847a0..6ca55f4b7bc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy @@ -4,17 +4,41 @@ import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.PrebidCache import org.prebid.server.functional.model.request.auction.PrebidCacheSettings import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO + class BidExpResponseSpec extends BaseSpec { - private static def hostBannerTtl = PBSUtils.randomNumber - private static def hostVideoTtl = PBSUtils.randomNumber - private static def cacheTtlService = pbsServiceFactory.getService(['cache.banner-ttl-seconds': hostBannerTtl as String, - 'cache.video-ttl-seconds' : hostVideoTtl as String]) + private static final def BANNER_TTL_HOST_CACHE = PBSUtils.randomNumber + private static final def VIDEO_TTL_HOST_CACHE = PBSUtils.randomNumber + private static final def BANNER_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def VIDEO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def AUDIO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def NATIVE_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final Map CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": BANNER_TTL_HOST_CACHE as String, + "cache.video-ttl-seconds" : VIDEO_TTL_HOST_CACHE as String] + private static final Map DEFAULT_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": BANNER_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.video" : VIDEO_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.native": NATIVE_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.audio" : AUDIO_TTL_DEFAULT_CACHE as String] + private static final Map EMPTY_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": "", + "cache.default-ttl-seconds.video" : "", + "cache.default-ttl-seconds.native": "", + "cache.default-ttl-seconds.audio" : ""] + private static final Map EMPTY_CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": "", + "cache.video-ttl-seconds" : ""] + private static def pbsOnlyHostCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + EMPTY_CACHE_TTL_CONFIG) + private static def pbsEmptyTtlService = pbsServiceFactory.getService(EMPTY_CACHE_TTL_CONFIG + EMPTY_CACHE_TTL_HOST_CONFIG) + private static def pbsHostAndDefaultCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG) + def "PBS auction should resolve bid.exp from response that is set by the bidderโ€™s adapter"() { given: "Default basicResponse with exp" @@ -131,25 +155,6 @@ class BidExpResponseSpec extends BaseSpec { assert response.seatbid.bid.first.exp == [bidRequestExp] } - def "PBS auction shouldn't resolve exp from request.ext.prebid.cache for request when it have invalid type"() { - given: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bid response shouldn't contain exp data" - assert !response.seatbid.first.bid.first.exp - - where: - bidRequest | cache - BidRequest.defaultBidRequest | new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) - BidRequest.defaultVideoRequest | new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) - } - def "PBS auction should resolve exp from account config for banner request when it have value"() { given: "default bidRequest" def bidRequest = BidRequest.defaultBidRequest @@ -173,28 +178,6 @@ class BidExpResponseSpec extends BaseSpec { assert response.seatbid.bid.first.exp == [accountCacheTtl] } - def "PBS auction shouldn't resolve exp from account videoCacheTtl config when bidRequest type doesn't matching"() { - given: "default bidRequest" - def bidRequest = BidRequest.defaultBidRequest - - and: "Account in the DB" - def auctionConfig = new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) - def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) - accountDao.save(account) - - and: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bid response shouldn't contain exp data" - assert !response.seatbid.first.bid.first.exp - } - def "PBS auction should resolve exp from account videoCacheTtl config for video request when it have value"() { given: "default bidRequest" def bidRequest = BidRequest.defaultVideoRequest @@ -218,55 +201,15 @@ class BidExpResponseSpec extends BaseSpec { assert response.seatbid.bid.first.exp == [accountCacheTtl] } - def "PBS auction should resolve exp from account bannerCacheTtl config for video request when it have value"() { - given: "default bidRequest" - def bidRequest = BidRequest.defaultVideoRequest - - and: "Account in the DB" - def accountCacheTtl = PBSUtils.randomNumber - def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) - def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) - accountDao.save(account) - - and: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bid response should contain exp data" - assert response.seatbid.bid.first.exp == [accountCacheTtl] - } - def "PBS auction should resolve exp from global banner config for banner request"() { given: "Default bidRequest" def bidRequest = BidRequest.defaultBidRequest when: "PBS processes auction request" - def response = cacheTtlService.sendAuctionRequest(bidRequest) - - then: "Bid response should contain exp data" - assert response.seatbid.bid.first.exp == [hostBannerTtl] - } - - def "PBS auction should resolve exp from global config for video request based on highest value"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultVideoRequest - - and: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = cacheTtlService.sendAuctionRequest(bidRequest) + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) then: "Bid response should contain exp data" - assert response.seatbid.bid.first.exp == [Math.max(hostVideoTtl, hostBannerTtl)] + assert response.seatbid.bid.first.exp == [BANNER_TTL_HOST_CACHE] } def "PBS auction should prioritize value from bid.exp rather than request.imp[].exp"() { @@ -356,9 +299,348 @@ class BidExpResponseSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = cacheTtlService.sendAuctionRequest(bidRequest) + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) then: "Bid response should contain exp data" assert response.seatbid.bid.first.exp == [accountCacheTtl] } + + def "PBS auction should prioritize bid.exp from the response over all other fields from the request and account config"() { + given: "Default bid request with specific imp media type" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0] = Imp.getDefaultImpression(mediaType).tap { + exp = PBSUtils.randomNumber + } + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber), + bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + and: "Default bid response with bid.exp" + def randomExp = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = randomExp + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp for #mediaType when the response, request, and account config don't include such data"() { + given: "Default bid request with specific imp media type" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response with bid.exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction should prioritize imp.exp and resolve bid.exp for #mediaType when request and account config include multiple exp sources"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType).tap { + exp = randomExp + } + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber), + bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + and: "Default bid response without bid.exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has #mediaType as mediaType"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: randomExp)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response?.seatbid?.first?.bid?.first?.exp + + where: + mediaType << [BANNER, NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has video as mediaType"() { + given: "Default bid request" + def bidsTtlSeconds = PBSUtils.randomNumber + def vastXmTtlSeconds = bidsTtlSeconds + 1 + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(VIDEO) + + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: vastXmTtlSeconds), + bids: new PrebidCacheSettings(ttlSeconds: bidsTtlSeconds)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == vastXmTtlSeconds + } + + def "PBS auction should resolve bid.exp when ext.prebid.cache.bids.ttlseconds is specified and no higher-priority fields are present"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: randomExp)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp when the account config and request imp type do not match"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType | auctionConfig + VIDEO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + VIDEO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: null) + BANNER | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + BANNER | new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + } + + def "PBS auction shouldn't resolve bid.exp when account config and request imp type match but account config for cache-ttl is not specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: null))) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [VIDEO, BANNER, NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp when account.auction.{banner/video}-cache-ttl and banner bid specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountAuctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == accountCacheTtl + + where: + mediaType | accountCacheTtl | accountAuctionConfig + BANNER | PBSUtils.randomNumber | new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + VIDEO | PBSUtils.randomNumber | new AccountAuctionConfig(videoCacheTtl: accountCacheTtl) + } + + def "PBS auction should resolve bid.exp when cache.{banner/video}-ttl-seconds config specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + enableCache() + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == expValue + + where: + mediaType | expValue + BANNER | BANNER_TTL_HOST_CACHE + VIDEO | VIDEO_TTL_HOST_CACHE + } + + def "PBS auction shouldn't resolve bid.exp when cache ttl-seconds is specified for #mediaType mediaType request"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + when: "PBS processes auction request" + def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp when cache.default-ttl-seconds.{banner,video,audio,native} is specified and no higher-priority fields are present"() { + given: "Prebid server with empty host config and default cache ttl config" + def config = EMPTY_CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG + def prebidServerService = pbsServiceFactory.getService(config) + + and: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == bidExpValue + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) + + where: + mediaType | bidExpValue + BANNER | BANNER_TTL_DEFAULT_CACHE + VIDEO | VIDEO_TTL_DEFAULT_CACHE + AUDIO | AUDIO_TTL_DEFAULT_CACHE + NATIVE | NATIVE_TTL_DEFAULT_CACHE + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy index fb3e2a3b0aa..579a8e16597 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy @@ -18,6 +18,8 @@ import spock.lang.PendingFeature import java.time.Instant import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.util.HttpUtil.REFERER_HEADER @@ -133,7 +135,7 @@ class BidValidationSpec extends BaseSpec { dooh.id = null dooh.venueType = null } - bidDoohRequest.ext.prebid.debug = 1 + bidDoohRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidDoohRequest) @@ -148,7 +150,7 @@ class BidValidationSpec extends BaseSpec { given: "Default basic BidRequest" def bidRequest = BidRequest.defaultBidRequest bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null) - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -159,9 +161,9 @@ class BidValidationSpec extends BaseSpec { } def "PBS should treat bids with 0 price as valid when deal id is present"() { - given: "Default basic BidRequest with generic bidder" + given: "Default basic BidRequest with generic bidder and enabled debug" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Bid response with 0 price bid" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) @@ -183,16 +185,21 @@ class BidValidationSpec extends BaseSpec { } def "PBS should drop invalid bid and emit debug error when bid price is #bidPrice and deal id is #dealId"() { - given: "Default basic BidRequest with generic bidder" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + given: "Default basic BidRequest with generic bidder and enabled debug" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.debug = debug + it.test = test + } and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - def bid = bidResponse.seatbid.first().bid.first() - bid.dealid = dealId - bid.price = bidPrice - def bidId = bid.id + def bidId = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.tap { + id = bidId + dealid = dealId + price = bidPrice + } + } and: "Set bidder response" bidder.setResponse(bidRequest.id, bidResponse) @@ -201,13 +208,61 @@ class BidValidationSpec extends BaseSpec { def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Invalid bid should be deleted" - assert response.seatbid.size() == 0 + assert !response.seatbid + assert !response.ext.seatnonbid and: "PBS should emit an error" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Dropped bid '$bidId'. Does not contain a positive (or zero if there is a deal) 'price'" as String] + where: + debug | test | bidPrice | dealId + DISABLED | ENABLED | PBSUtils.randomNegativeNumber | null + DISABLED | ENABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + DISABLED | ENABLED | 0 | null + DISABLED | ENABLED | null | PBSUtils.randomNumber + DISABLED | ENABLED | null | null + ENABLED | DISABLED | PBSUtils.randomNegativeNumber | null + ENABLED | DISABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + ENABLED | DISABLED | 0 | null + ENABLED | DISABLED | null | PBSUtils.randomNumber + ENABLED | DISABLED | null | null + } + + def "PBS should drop invalid bid without debug error when request debug disabled and bid price is #bidPrice and deal id is #dealId"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + test = DISABLED + ext.prebid.debug = DISABLED + } + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.tap { + dealid = dealId + price = bidPrice + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Invalid bid should be deleted" + assert !response.seatbid + assert !response.ext.seatnonbid + + and: "PBS shouldn't emit an error" + assert !response.ext?.warnings + assert !response.ext?.warnings + + and: "PBS should call bidder" + def bidderRequests = bidder.getBidderRequests(bidResponse.id) + assert bidderRequests.size() == 1 + where: bidPrice | dealId PBSUtils.randomNegativeNumber | null @@ -220,7 +275,7 @@ class BidValidationSpec extends BaseSpec { def "PBS should only drop invalid bid without discarding whole seat"() { given: "Default basic BidRequest with generic bidder" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED bidRequest.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] and: "Bid response with 2 bids" @@ -239,7 +294,7 @@ class BidValidationSpec extends BaseSpec { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) - then: "Invalid bids should be deleted" + then: "Bid response contains only valid bid" assert response.seatbid?.first()?.bid*.id == [validBidId] and: "PBS should emit an error" @@ -247,6 +302,53 @@ class BidValidationSpec extends BaseSpec { assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Dropped bid '$invalidBid.id'. Does not contain a positive (or zero if there is a deal) 'price'" as String] + where: + debug | test | bidPrice | dealId + 0 | 1 | PBSUtils.randomNegativeNumber | null + 0 | 1 | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + 0 | 1 | 0 | null + 0 | 1 | null | PBSUtils.randomNumber + 0 | 1 | null | null + 1 | 0 | PBSUtils.randomNegativeNumber | null + 1 | 0 | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + 1 | 0 | 0 | null + 1 | 0 | null | PBSUtils.randomNumber + 1 | 0 | null | null + } + + def "PBS should only drop invalid bid without discarding whole seat without debug error when request debug disabled "() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + test = DISABLED + ext.prebid.tap { + debug = DISABLED + multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] + } + } + + and: "Bid response with 2 bids" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidResponse.seatbid[0].bid << Bid.getDefaultBid(bidRequest.imp.first()) + + and: "One of the bids is invalid" + def invalidBid = bidResponse.seatbid.first().bid.first() + invalidBid.dealid = dealId + invalidBid.price = bidPrice + def validBidId = bidResponse.seatbid.first().bid.last().id + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response contains only valid bid" + assert response.seatbid?.first()?.bid*.id == [validBidId] + + and: "PBS shouldn't emit an error" + assert !response.ext?.warnings + assert !response.ext?.warnings + where: bidPrice | dealId PBSUtils.randomNegativeNumber | null @@ -257,10 +359,7 @@ class BidValidationSpec extends BaseSpec { } def "PBS should update 'adapter.generic.requests.bid_validation' metric when bid validation error appears"() { - given: "Initial 'adapter.generic.requests.bid_validation' metric value" - def initialMetricValue = getCurrentMetricValue(defaultPbsService, "adapter.generic.requests.bid_validation") - - and: "Bid request" + given: "Bid request" def bidRequest = BidRequest.defaultBidRequest and: "Set invalid bid response" @@ -269,12 +368,15 @@ class BidValidationSpec extends BaseSpec { } bidder.setResponse(bidRequest.id, bidResponse) + and: "Flush metric" + flushMetrics(defaultPbsService) + when: "Sending auction request to PBS" defaultPbsService.sendAuctionRequest(bidRequest) then: "Bid validation metric value is incremented" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics["adapter.generic.requests.bid_validation"] == initialMetricValue + 1 + assert metrics["adapter.generic.requests.bid_validation"] == 1 } def "PBS shouldn't throw error when two separate eids with same eids.source"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy index deec160409d..bd7d6abfafc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy @@ -26,6 +26,8 @@ import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE import static org.prebid.server.functional.model.config.BidValidationEnforcement.SKIP import static org.prebid.server.functional.model.config.BidValidationEnforcement.WARN +import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC class BidderFormatSpec extends BaseSpec { @@ -66,7 +68,7 @@ class BidderFormatSpec extends BaseSpec { def exception = thrown(PrebidServerException) assert exception.statusCode == 400 assert exception.responseBody == "Invalid request format: " + - "Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties" + "request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties" where: bannerFormatWeight | bannerFormatHeight @@ -90,7 +92,7 @@ class BidderFormatSpec extends BaseSpec { then: "PBs should throw error due to banner.format{w.h} validation" def exception = thrown(PrebidServerException) assert exception.statusCode == 400 - assert exception.responseBody == "Invalid request format: Request imp[0].banner.format[0] " + + assert exception.responseBody == "Invalid request format: request.imp[0].banner.format[0] " + "should define *either* {w, h} (for static size requirements) " + "*or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive" @@ -580,13 +582,13 @@ class BidderFormatSpec extends BaseSpec { assert !bidder.getBidderRequests(bidRequest.id) where: - secure | secureMarkup - 1 | SKIP.value - 1 | ENFORCE.value - 1 | WARN.value - 0 | SKIP.value - 0 | ENFORCE.value - 0 | WARN.value + secure | secureMarkup + SECURE | SKIP.value + SECURE | ENFORCE.value + SECURE | WARN.value + NON_SECURE | SKIP.value + NON_SECURE | ENFORCE.value + NON_SECURE | WARN.value } def "PBS should emit metrics and error when imp[0].secure = 1 and config WARN and bid response adm contain #url"() { @@ -596,7 +598,7 @@ class BidderFormatSpec extends BaseSpec { and: "Default bid request with secure and banner or video or nativeObj" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].secure = 1 + imp[0].secure = SECURE imp[0].banner = banner imp[0].video = video imp[0].nativeObj = nativeObj @@ -654,7 +656,7 @@ class BidderFormatSpec extends BaseSpec { and: "Default bid request with secure and banner or video or nativeObj" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].secure = 1 + imp[0].secure = SECURE imp[0].banner = banner imp[0].video = video imp[0].nativeObj = nativeObj @@ -704,7 +706,7 @@ class BidderFormatSpec extends BaseSpec { and: "Default bid request with secure and banner or video or nativeObj" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].secure = 1 + imp[0].secure = SECURE imp[0].banner = banner imp[0].video = video imp[0].nativeObj = nativeObj @@ -796,16 +798,16 @@ class BidderFormatSpec extends BaseSpec { assert !bidder.getBidderRequests(bidRequest.id) where: - url | secure | secureMarkup - "http%3A" | 0 | SKIP.value - "http" | 0 | SKIP.value - "https" | 1 | SKIP.value - "http%3A" | 0 | WARN.value - "http" | 0 | WARN.value - "https" | 1 | WARN.value - "http%3A" | 0 | ENFORCE.value - "http" | 0 | ENFORCE.value - "https" | 1 | ENFORCE.value + url | secure | secureMarkup + "http%3A" | NON_SECURE | SKIP.value + "http" | NON_SECURE | SKIP.value + "https" | SECURE | SKIP.value + "http%3A" | NON_SECURE | WARN.value + "http" | NON_SECURE | WARN.value + "https" | SECURE | WARN.value + "http%3A" | NON_SECURE | ENFORCE.value + "http" | NON_SECURE | ENFORCE.value + "https" | SECURE | ENFORCE.value } def "PBS should ignore specified secureMarkup #secureMarkup validation when secure is 0"() { @@ -816,7 +818,7 @@ class BidderFormatSpec extends BaseSpec { def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].tap { - secure = 0 + secure = NON_SECURE ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index c31570e26ed..cab50bd816b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredImp @@ -16,30 +17,37 @@ import org.prebid.server.functional.model.request.auction.ImpExtContext import org.prebid.server.functional.model.request.auction.ImpExtContextData import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidStoredRequest -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent +import static org.prebid.server.functional.model.Currency.CHF +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.CompressionType.GZIP import static org.prebid.server.functional.model.bidder.CompressionType.NONE import static org.prebid.server.functional.model.request.auction.Asset.titleAsset import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY +import static org.prebid.server.functional.model.response.auction.ErrorType.ALIAS +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.HttpUtil.CONTENT_ENCODING_HEADER import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED @@ -56,7 +64,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain httpcalls" - assert response.ext?.debug?.httpcalls[GENERIC.value] + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] and: "Response should not contain error" assert !response.ext?.errors @@ -82,7 +90,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [2] + assert response.ext?.errors[GENERIC]*.code == [2] where: adapterDefault | generic | adapterConfig @@ -157,7 +165,7 @@ class BidderParamsSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - bidRequest.regs.ext = new RegsExt(usPrivacy: validCcpa) + bidRequest.regs.usPrivacy = validCcpa def lat = PBSUtils.getRandomDecimal(0, 90) def lon = PBSUtils.getRandomDecimal(0, 90) bidRequest.device = new Device(geo: new Geo(lat: lat, lon: lon)) @@ -184,7 +192,7 @@ class BidderParamsSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - bidRequest.regs.ext = new RegsExt(usPrivacy: validCcpa) + bidRequest.regs.usPrivacy = validCcpa def lat = PBSUtils.getRandomDecimal(0, 90) as float def lon = PBSUtils.getRandomDecimal(0, 90) as float bidRequest.device = new Device(geo: new Geo(lat: lat, lon: lon)) @@ -210,7 +218,7 @@ class BidderParamsSpec extends BaseSpec { bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(firstParam: firstParam) and: "Set bidderParam to bidRequest" - bidRequest.ext.prebid.bidderParams = [(GENERIC): [firstParam: PBSUtils.randomNumber]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [firstParam: PBSUtils.randomNumber]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -245,7 +253,7 @@ class BidderParamsSpec extends BaseSpec { and: "Set bidderParam to bidRequest" def secondParam = PBSUtils.randomNumber - bidRequest.ext.prebid.bidderParams = [(GENERIC): [secondParam: secondParam]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [secondParam: secondParam]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -287,8 +295,8 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [999] - assert response.ext?.errors[ErrorType.GENERIC]*.message == ["host name must not be empty"] + assert response.ext?.errors[GENERIC]*.code == [999] + assert response.ext?.errors[GENERIC]*.message == ["host name must not be empty"] } def "PBS should reject bidder when bidder params from request doesn't satisfy json-schema for auction request"() { @@ -393,8 +401,8 @@ class BidderParamsSpec extends BaseSpec { assert response.seatbid.isEmpty() and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == ["Bidder does not support any media types."] + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Bidder does not support any media types."] where: configMediaType | bidRequest @@ -510,8 +518,8 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequest.imp[0].nativeObj and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from the " + "request for this bidder." as String] @@ -529,7 +537,7 @@ class BidderParamsSpec extends BaseSpec { def bidResponse = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert bidResponse.ext?.warnings[ErrorType.GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") + assert bidResponse.ext?.warnings[GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") and: "Bid response shouldn't contain any seatbid" assert !bidResponse.seatbid @@ -563,7 +571,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert response.ext?.warnings[ErrorType.GENERIC]?.message == + assert response.ext?.warnings[GENERIC]?.message == ["Imp ${bidRequest.imp[1].id} does not have a supported media type and has been removed from the request for this bidder."] and: "Bid response should contain seatbid" @@ -598,8 +606,8 @@ class BidderParamsSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 0 and: "Response should contain errors" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2, 2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2, 2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from " + "the request for this bidder.", "Bid request contains 0 impressions after filtering."] @@ -644,7 +652,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain header Content-Encoding = gzip" - assert response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER)?.first() == compressionType } @@ -660,7 +668,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should not contain header Content-Encoding" - assert !response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert !response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER) } @@ -701,9 +709,9 @@ class BidderParamsSpec extends BaseSpec { where: secureStoredRequest | secureBidderRequest - null | 1 - 1 | 1 - 0 | 0 + null | SECURE + SECURE | SECURE + NON_SECURE | NON_SECURE } def "PBS auction should populate imp[0].secure depend which value in imp request"() { @@ -721,9 +729,9 @@ class BidderParamsSpec extends BaseSpec { where: secureRequest | secureBidderRequest - null | 1 - 1 | 1 - 0 | 0 + null | SECURE + SECURE | SECURE + NON_SECURE | NON_SECURE } def "PBS shouldn't emit warning and proceed auction when imp.ext.anyUnsupportedBidder and imp.ext.prebid.bidder.generic in the request"() { @@ -803,4 +811,233 @@ class BidderParamsSpec extends BaseSpec { tid == impExt.tid } } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "") + + and: "Default bid request with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "") + + and: "Default bid request with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + and: "PBS should emit an warnings" + assert response.ext?.warnings[GENERIC]*.code == [999] + assert response.ext?.warnings[GENERIC]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.aliases.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "PBS should emit an warnings" + assert response.ext?.warnings[ALIAS]*.code == [999] + assert response.ext?.warnings[ALIAS]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.ALIAS.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index ccd5f8b9cf0..4f8dcf7675e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -19,6 +19,8 @@ import static org.prebid.server.functional.model.response.auction.MediaType.VIDE class CacheSpec extends BaseSpec { + private final static String PBS_API_HEADER = 'x-pbc-api-key' + def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { given: "Current value of metric prebid_cache.requests.ok" def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok") @@ -87,6 +89,51 @@ class CacheSpec extends BaseSpec { then: "PBS should call PBC" assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call shouldn't include api-key" + assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + } + + def "PBS should cache bids without api-key header when targeting is specified and api-key-secured disabled"() { + given: "Pbs config with disabled api-key-secured and pbc.api.key" + def apiKey = PBSUtils.randomString + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'false']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.enableCache() + bidRequest.ext.prebid.targeting = new Targeting() + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + prebidCache.getRequest() + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call shouldn't include api-key" + assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + } + + def "PBS should cache bids with api-key header when targeting is specified and api-key-secured enabled"() { + given: "Pbs config with api-key-secured and pbc.api.key" + def apiKey = PBSUtils.randomString + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'true']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.enableCache() + bidRequest.ext.prebid.targeting = new Targeting() + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + prebidCache.getRequest() + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call should include api-key" + assert prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] == [apiKey] } def "PBS should not cache bids when targeting isn't specified"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy index 568f202be55..df5bba70028 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -14,6 +14,7 @@ import static org.prebid.server.functional.model.Currency.CHF import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer class CurrencySpec extends BaseSpec { @@ -146,6 +147,49 @@ class CurrencySpec extends BaseSpec { CHF || EUR } + def "PBS should emit warning when request contain more that one currency"() { + given: "Default BidRequest with currencies" + def currencies = [EUR, USD] + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = currencies + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain first requested currency" + assert bidResponse.cur == currencies[0] + + and: "Bidder request should contain requested currencies" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == currencies + + and: "Bid response should contain warnings" + assert bidResponse.ext.warnings[GENERIC]?.message == ["a single currency (${currencies[0]}) has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency." as String] + } + + def "PBS shouldn't emit warning when request contain one currency"() { + given: "Default BidRequest with currency" + def currency = [USD] + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = currency + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain first requested currency" + assert bidResponse.cur == currency[0] + + and: "Bidder request should contain requested currency" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == currency + + and: "Bid response shouldn't contain warnings" + assert !bidResponse.ext.warnings + } + private static Map getExternalCurrencyConverterConfig() { ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, "currency-converter.external-rates.enabled" : "true", diff --git a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy index 3f7682eb708..ea169ad00ee 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy @@ -3,41 +3,63 @@ package org.prebid.server.functional.tests import org.apache.commons.lang3.StringUtils import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountMetricsConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.BASIC +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.NONE +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.response.auction.BidderCallType.STORED_BID_RESPONSE class DebugSpec extends BaseSpec { private static final String overrideToken = PBSUtils.randomString + private static final String ACCOUNT_METRICS_PREFIX_NAME = "account" + private static final String DEBUG_REQUESTS_METRIC = "debug_requests" + private static final String ACCOUNT_DEBUG_REQUESTS_METRIC = "account.%s.debug_requests" + private static final String REQUEST_OK_WEB_METRICS = "requests.ok.openrtb2-web" - def "PBS should return debug information when debug flag is #debug and test flag is #test"() { + def "PBS should return debug information and emit metrics when debug flag is #debug and test flag is #test"() { given: "Default BidRequest with test flag" def bidRequest = BidRequest.defaultBidRequest bidRequest.ext.prebid.debug = debug bidRequest.test = test + and: "Flash metrics" + flushMetrics(defaultPbsService) + when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Response should contain ext.debug" assert response.ext?.debug + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + and: "Account debug metrics shouldn't be incremented" + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + where: - debug | test - 1 | null - 1 | 0 - null | 1 + debug | test + ENABLED | null + ENABLED | DISABLED + null | ENABLED } def "PBS shouldn't return debug information when debug flag is #debug and test flag is #test"() { @@ -46,16 +68,27 @@ class DebugSpec extends BaseSpec { bidRequest.ext.prebid.debug = test bidRequest.test = test + and: "Flash metrics" + flushMetrics(defaultPbsService) + when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Response shouldn't contain ext.debug" assert !response.ext?.debug + and: "Debug metrics shouldn't be populated" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[DEBUG_REQUESTS_METRIC] + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + + and: "General metrics should be present" + assert metricsRequest[REQUEST_OK_WEB_METRICS] == 1 + where: - debug | test - 0 | null - null | 0 + debug | test + DISABLED | null + null | DISABLED } def "PBS should not return debug information when bidder-level setting debug.allowed = false"() { @@ -64,7 +97,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -84,7 +117,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -102,7 +135,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) @@ -132,7 +165,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) @@ -161,7 +194,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) @@ -183,7 +216,7 @@ class DebugSpec extends BaseSpec { def "PBS should use default values = true for bidder-level setting debug.allow and account-level setting debug-allowed when they are not specified"() { given: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) @@ -201,7 +234,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: debugAllowedAccount)) @@ -233,7 +266,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) @@ -278,11 +311,11 @@ class DebugSpec extends BaseSpec { assert response.ext?.debug where: - requestDebug || storedRequestDebug - 1 || 0 - 1 || 1 - 1 || null - null || 1 + requestDebug | storedRequestDebug + ENABLED | DISABLED + ENABLED | ENABLED + ENABLED | null + null | ENABLED } def "PBS AMP shouldn't return debug information when request flag is #requestDebug and stored request flag is #storedRequestDebug"() { @@ -307,12 +340,12 @@ class DebugSpec extends BaseSpec { assert !response.ext?.debug where: - requestDebug || storedRequestDebug - 0 || 1 - 0 || 0 - 0 || null - null || 0 - null || null + requestDebug | storedRequestDebug + DISABLED | ENABLED + DISABLED | DISABLED + DISABLED | null + null | DISABLED + null | null } def "PBS shouldn't populate call type when it's default bidder call"() { @@ -349,4 +382,157 @@ class DebugSpec extends BaseSpec { and: "Response should not contain ext.warnings" assert !response.ext?.warnings } + + def "PBS should return debug information and emit metrics when account debug enabled and verbosity detailed"() { + given: "Default basic generic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: DETAILED), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] == 1 + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + } + + def "PBS shouldn't return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() { + given: "Default basic generic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Account debug metrics shouldn't be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] + + and: "Request debug metrics should be incremented" + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + where: + verbosityLevel << [NONE, BASIC] + } + + def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity detailed"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: DETAILED), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] == 1 + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + } + + def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Account debug metrics shouldn't be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] + + and: "Debug metrics should be incremented" + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + where: + verbosityLevel << [NONE, BASIC] + } + + def "PBS shouldn't emit auction request metric when incoming request invalid"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null) + bidRequest.ext.prebid.debug = ENABLED + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.responseBody.contains("request.site should include at least one of request.site.id or request.site.page") + + and: "Debug metrics shouldn't be populated" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[DEBUG_REQUESTS_METRIC] + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + + and: "General metrics shouldn't be present" + assert !metricsRequest[REQUEST_OK_WEB_METRICS] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy index 339a2637472..529f9c9c961 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy @@ -17,10 +17,13 @@ import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class EidsSpec extends BaseSpec { + private static final String EMPTY_STRING = "" + def "PBS shouldn't populate user.id from user.ext data"() { given: "Default basic BidRequest with generic bidder" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -197,4 +200,76 @@ class EidsSpec extends BaseSpec { and: "Alias bidder should contain one eids" assert sortedEids[1].eids == [eid] } + + def "PBS should populate warning for one removed UID when invalid uidId"() { + given: "BidRequest with eids" + def sourceId = PBSUtils.randomString + def validUidId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: sourceId, + uids: [new Uid(id: invalidUidId), + new Uid(id: validUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids.uids.id.flatten() == [validUidId] + + and: "Bid response should contain warning" + assert bidResponse.ext.warnings[PREBID]?.code == [999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["removed EID ${sourceId} due to empty ID" as String] + + where: + invalidUidId << [EMPTY_STRING, null] + } + + def "PBS should populate warnings for removed UIDs and entire eids when requested invalid uidIds"() { + given: "BidRequest with eids" + def sourceId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: sourceId, + uids: [new Uid(id: invalidUidId), + new Uid(id: invalidUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + + and: "Bid response should contain warnings" + assert bidResponse.ext.warnings[PREBID]?.code == [999, 999, 999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["removed EID ${sourceId} due to empty ID" as String, + "removed EID ${sourceId} due to empty ID" as String, + "removed empty EID array" as String] + + where: + invalidUidId << [EMPTY_STRING, null] + } + + def "PBS shouldn't populate warning for UID when Uid id is valid"() { + given: "BidRequest with eids" + def validUidId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: PBSUtils.randomString, + uids: [new Uid(id: validUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids.uids.id.flatten() == [validUidId] + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy index 782b965715d..1d36b8beadc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.Account @@ -11,6 +12,7 @@ import org.prebid.server.functional.model.request.auction.BidderControls import org.prebid.server.functional.model.request.auction.GenericPreferredBidder import org.prebid.server.functional.model.request.auction.Native +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER @@ -52,7 +54,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -62,6 +64,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with one requested preferred media type when default adapters multi format is false in config and preferred media type specified at account level"() { @@ -98,7 +106,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -108,6 +116,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with all requested media type when multi format is true in config and preferred media type specified at request level"() { @@ -119,7 +133,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -129,6 +143,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp.banner assert bidderRequest.imp.audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with all requested media type when multi format is true in config and preferred media type specified at account level"() { @@ -190,7 +210,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -200,6 +220,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with warning and don't make a bidder call when multi format at request and preferred media type specified at account level with non requested media type"() { @@ -241,7 +267,7 @@ class FilterMultiFormatSpec extends BaseSpec { imp[0].banner = null imp[0].audio = Audio.defaultAudio imp[0].nativeObj = Native.defaultNative - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -254,6 +280,12 @@ class FilterMultiFormatSpec extends BaseSpec { assert bidResponse.ext.warnings[GENERIC]?.message == ["Imp ${bidRequest.imp[0].id} does not have a media type after filtering and has been removed from the request for this bidder.", "Bid request contains 0 impressions after filtering."] + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and preferred media type specified at account level"() { @@ -292,7 +324,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = null imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -304,6 +336,12 @@ class FilterMultiFormatSpec extends BaseSpec { and: "Bid response shouldn't contain warning" assert !bidResponse.ext.warnings + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and multi format is false and preferred media type specified at request level with null"() { @@ -315,7 +353,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.getDefaultBanner() imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: NULL)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -326,6 +364,12 @@ class FilterMultiFormatSpec extends BaseSpec { and: "Bid response shouldn't contain warning" assert !bidResponse.ext?.warnings + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: NULL)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: NULL)), + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and multi format is false and preferred media type specified at account level with null"() { @@ -364,7 +408,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } and: "Account in the DB with preferred media type" @@ -379,5 +423,53 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] + } + + def "PBS should not preferred media type specified at request level when it's alias bidder"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapter-defaults.ortb.multiformat-supported": "false", + "adapters.generic.ortb.multiformat-supported": "false") + + and: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = Banner.defaultBanner + audio = Audio.defaultAudio + ext.prebid.bidder.tap { + alias = new Generic() + generic = null + } + } + ext.prebid.tap { + it.aliases = [(ALIAS.value): BidderName.GENERIC] + it.bidderControls = bidderControls + } + } + + and: "Account in the DB with preferred media type" + def accountConfig = new AccountAuctionConfig(preferredMediaType: [(BidderName.GENERIC): AUDIO]) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain preferred media type from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.imp[0].banner + assert bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy index 99cd8831745..caabebba8ff 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy @@ -32,7 +32,7 @@ class HttpSettingsSpec extends BaseSpec { def "PBS should take account information from http data source on auction request"() { given: "Get basic BidRequest with generic bidder and set gdpr = 1" def bidRequest = BidRequest.defaultBidRequest - bidRequest.regs.ext.gdpr = 1 + bidRequest.regs.gdpr = 1 and: "Prepare default account response with gdpr = 0" def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest?.site?.publisher?.id) @@ -61,7 +61,7 @@ class HttpSettingsSpec extends BaseSpec { and: "Get basic stored request and set gdpr = 1" def ampStoredRequest = BidRequest.defaultBidRequest ampStoredRequest.site.publisher.id = ampRequest.account - ampStoredRequest.regs.ext.gdpr = 1 + ampStoredRequest.regs.gdpr = 1 and: "Save storedRequest into DB" def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy new file mode 100644 index 00000000000..085bd19a690 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy @@ -0,0 +1,295 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Pmp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.EMPTY +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.RUBICON +import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class ImpRequestSpec extends BaseSpec { + + private final PrebidServerService defaultPbsServiceWithAlias = pbsServiceFactory.getService(GENERIC_ALIAS_CONFIG) + private static final String EMPTY_ID = "" + + def "PBS should update imp fields when imp.ext.prebid.imp contain bidder information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = Pmp.defaultPmp + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + ext.prebid.imp = [(bidderName): new Imp(pmp: extPmp)] + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [extPmp] + + and: "BidderRequest should contain original stored request id" + assert bidderRequest.imp.ext.prebid.storedRequest.id == [storedRequestId] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert bidderRequest?.imp?.ext?.prebid?.imp == [null] + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should update only required imp when it contain bidder information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def impWithParameters = Imp.defaultImpression.tap { + pmp = Pmp.defaultPmp + ext.prebid.imp = [(bidderName): new Imp(pmp: extPmp)] + } + def impWithoutParameters = Imp.defaultImpression.tap { + pmp = Pmp.defaultPmp + } + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [impWithParameters, impWithoutParameters] + } + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value only for required imp" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.find { it.id == impWithParameters.id }?.pmp == extPmp + assert bidderRequest.imp.find { it.id == impWithoutParameters.id }?.pmp == impWithoutParameters.pmp + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should update imp fields when imp.ext.prebid.imp contain bidder alias information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = Pmp.defaultPmp + ext.prebid.imp = [(aliasName): new Imp(pmp: extPmp)] + } + ext.prebid.aliases = [(aliasName.value): GENERIC] + } + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [extPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + aliasName | bidderName + ALIAS | GENERIC + ALIAS_CAMEL_CASE | GENERIC + ALIAS | GENERIC_CAMEL_CASE + ALIAS_CAMEL_CASE | GENERIC_CAMEL_CASE + } + + def "PBS shouldn't update imp fields when imp.ext.prebid.imp contain only bidder with invalid name"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(bidderName): new Imp(pmp: Pmp.defaultPmp)] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["WARNING: request.imp[0].ext.prebid.imp.${bidderName} was dropped with the reason: invalid bidder"] + + and: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [WILDCARD, UNKNOWN] + } + + def "PBS shouldn't update imp fields and without warning when imp.ext.prebid.imp contain not applicable bidder"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(RUBICON): new Imp(pmp: Pmp.defaultPmp)] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain warning" + assert !response?.ext?.warnings + + and: "BidderRequest should contain pmp from original imp" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + } + + def "PBS should always update specified bidder imp when imp.ext.prebid.imp contain such bidder"() { + given: "PBs with openx bidder" + def pbsService = pbsServiceFactory.getService( + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + + and: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def extPrebidImpPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.imp = [(OPENX): new Imp(pmp: extPrebidImpPmp)] + } + } + + when: "Requesting PBS auction" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain warning" + assert !response?.ext?.warnings + + and: "Generic bidderRequest should contain pmp from original imp" + def bidderToBidderRequests = getRequests(response) + assert bidderToBidderRequests[GENERIC.value].first.imp.pmp == [impPmp] + + and: "OpenX bidderRequest should contain pmp from ext.prebid.imp" + assert bidderToBidderRequests[OPENX.value].first.imp.pmp == [extPrebidImpPmp] + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequests" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert !bidderRequests?.imp?.ext?.prebid?.imp?.flatten() + } + + def "PBS should validate imp and add proper warning when imp.ext.prebid.imp contain invalid ortb data"() { + given: "BidRequest with invalid config for ext.prebid.imp" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(GENERIC): Imp.defaultImpression.tap { + id = EMPTY_ID + }] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["imp.ext.prebid.imp.generic can not be merged into original imp [id=${bidRequest.imp.first.id}], " + + "reason: imp[id=] missing required field: \"id\""] + + and: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + } + + def "PBS shouldn't update imp fields when imp.ext.prebid.imp contain invalid empty data"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = prebidImp + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "BidderRequest should contain original stored request id" + assert bidderRequest.imp.ext.prebid.storedRequest.id == [storedRequestId] + + and: "PBS should remove imp.ext.prebid.imp.pmp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp?.get(GENERIC)?.pmp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + prebidImp << [ + null, + [:], + [(EMPTY): new Imp(pmp: Pmp.defaultPmp)], + [(GENERIC): null], + [(GENERIC): new Imp()], + [(GENERIC): new Imp(pmp: new Pmp())] + ] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 856315e17d2..21bb80df135 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -17,6 +17,7 @@ import org.prebid.server.functional.model.request.auction.RefSettings import org.prebid.server.functional.model.request.auction.RefType import org.prebid.server.functional.model.request.auction.Refresh import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Source import org.prebid.server.functional.model.request.auction.SourceType import org.prebid.server.functional.model.request.auction.User @@ -46,10 +47,7 @@ class OrtbConverterSpec extends BaseSpec { def usPrivacyRandomString = PBSUtils.randomString def gdpr = 0 def bidRequest = BidRequest.defaultBidRequest.tap { - regs = Regs.defaultRegs.tap { - it.usPrivacy = usPrivacyRandomString - it.gdpr = gdpr - } + regs = new Regs(usPrivacy: usPrivacyRandomString, gdpr: gdpr) } when: "Requesting PBS auction with ortb 2.6" @@ -182,7 +180,7 @@ class OrtbConverterSpec extends BaseSpec { } } - def "PBS should move eids to o user.ext.eids when adapter doesn't support ortb 2.6"() { + def "PBS should move eids to user.ext.eids when adapter doesn't support ortb 2.6"() { given: "Default bid request with user.eids" def defaultEids = [Eid.defaultEid] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -563,6 +561,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber] } } @@ -586,6 +585,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber, PBSUtils.randomNumber] } } @@ -1140,7 +1140,7 @@ class OrtbConverterSpec extends BaseSpec { def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { regs = Regs.defaultRegs.tap { - ext.gpc = randomGpc + ext = new RegsExt(gpc: randomGpc) } } @@ -1156,7 +1156,7 @@ class OrtbConverterSpec extends BaseSpec { def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { regs = Regs.defaultRegs.tap { - ext.gpc = randomGpc + ext = new RegsExt(gpc: randomGpc) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy index 9e942f7c3c7..f6b02f22e76 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy @@ -1,28 +1,46 @@ package org.prebid.server.functional.tests import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountBidValidationConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.util.PBSUtils +import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.NO_BID -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.OTHER_ERROR -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_MEDIA_TYPE -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.TIMED_OUT +import static org.mockserver.model.HttpStatusCode.PROCESSING_102 +import static org.mockserver.model.HttpStatusCode.SERVICE_UNAVAILABLE_503 +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_BIDDER_UNREACHABLE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_INVALID_BID_RESPONSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_NO_BID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_TIMED_OUT +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC class SeatNonBidSpec extends BaseSpec { def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder didn't bid"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true - } + def bidRequest = requestWithAllBidStatus and: "Default bidder response without bid" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -41,24 +59,21 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == NO_BID + assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID where: responseStatusCode << [OK_200, NO_CONTENT_204] } - def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with non-SUCCESS status code"() { + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with invalid bid response status code"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true - } + def bidRequest = requestWithAllBidStatus - and: "Default bidder response without bid" + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) and: "Set bidder response" - def successStatuses = [OK_200, NO_CONTENT_204] - def statusCode = PBSUtils.getRandomElement(HttpStatusCode.values() - successStatuses as List) + def statusCode = PBSUtils.getRandomElement([PROCESSING_102, BAD_REQUEST_400, INTERNAL_SERVER_ERROR_500]) bidder.setResponse(bidRequest.id, bidResponse, statusCode) when: "PBS processes auction request" @@ -70,16 +85,103 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == OTHER_ERROR + assert seatNonBid.nonBid[0].statusCode == ERROR_INVALID_BID_RESPONSE } - def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() { + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with bidder unreachable status code"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse, SERVICE_UNAVAILABLE_503) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == ERROR_BIDDER_UNREACHABLE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with invalid creative size status code"() { + given: "Default bid request with returnAllBidStatus" + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response with creative size adjustment" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.tap { + bid.first.height = bidRequest.imp.first.banner.format.first.height + 1 + bid.first.weight = bidRequest.imp.first.banner.format.first.weight + 1 + } } - and: "Default bidder response without bid" + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(bidValidations: + new AccountBidValidationConfig(bannerMaxSizeEnforcement: ENFORCE))) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_SIZE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with not secure status code"() { + given: "PBS with secure-markup enforcement" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": ENFORCE.value]) + + and: "A bid request with secure and returnAllBidStatus flags set" + def bidRequest = requestWithAllBidStatus.tap { + imp[0].secure = SECURE + } + + and: "A default bidder response without a valid bid" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.bid.first.tap { + it.adm = new Adm(assets: [Asset.getImgAsset("http://secure-assets.${PBSUtils.randomString}.com")]) + } + } + + and: "Setting the bidder response" + bidder.setResponse(bidRequest.id, storedBidResponse) + + when: "PBS processes the auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "The PBS response should contain seatNonBid for the called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE + + and: "PBS response shouldn't contain seatBid" + assert !response.seatbid + } + + def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() { + given: "Default bid request with returnAllBidStatus" + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) and: "Set bidder response" @@ -95,12 +197,11 @@ class SeatNonBidSpec extends BaseSpec { def "PBS should populate seatNonBid when returnAllBidStatus=true and debug=#debug and requested bidder didn't bid for any reason"() { given: "Default bid request with returnAllBidStatus and debug = #debug" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus.tap { ext.prebid.debug = debug } - and: "Default bidder response without bid" + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { seatbid = [] } @@ -117,13 +218,13 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == NO_BID + assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID and: "PBS response shouldn't contain seatBid" assert !response.seatbid where: - debug << [1, 0, null] + debug << [ENABLED, DISABLED, null] } def "PBS shouldn't populate seatNonBid when returnAllBidStatus=false and debug=#debug and requested bidder didn't bid for any reason"() { @@ -149,7 +250,7 @@ class SeatNonBidSpec extends BaseSpec { assert !response.seatbid where: - debug << [1, 0, null] + debug << [ENABLED, DISABLED, null] } def "PBS should populate seatNonBid when bidder is rejected due to timeout"() { @@ -158,8 +259,7 @@ class SeatNonBidSpec extends BaseSpec { def pbsService = pbsServiceFactory.getService(["auction.biddertmax.min": timeout as String]) and: "Default bid request with max timeout" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus.tap { tmax = timeout } @@ -179,7 +279,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == TIMED_OUT + assert seatNonBid.nonBid[0].statusCode == ERROR_TIMED_OUT } def "PBS should populate seatNonBid when filter-imp-media-type=true and imp doesn't contain supported media type"() { @@ -203,7 +303,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_BY_MEDIA_TYPE + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE and: "seatbid should be empty" assert response.seatbid.isEmpty() @@ -230,4 +330,10 @@ class SeatNonBidSpec extends BaseSpec { assert !response.ext.seatnonbid assert response.seatbid } + + private static BidRequest getRequestWithAllBidStatus(DistributionChannel channel = SITE) { + BidRequest.getDefaultBidRequest(channel).tap { + ext.prebid.returnAllBidStatus = true + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy index e2a8a39ffef..767c4b8e544 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy @@ -2,11 +2,15 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature @@ -14,6 +18,8 @@ import static org.prebid.server.functional.model.bidder.BidderName.GENERIC class StoredResponseSpec extends BaseSpec { + private final PrebidServerService pbsService = pbsServiceFactory.getService(["cache.default-ttl-seconds.banner": ""]) + @PendingFeature def "PBS should not fail auction with storedAuctionResponse when request bidder params doesn't satisfy json-schema"() { given: "BidRequest with bad bidder datatype and storedAuctionResponse" @@ -30,7 +36,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should not contain errors and warnings" assert !response.ext?.errors @@ -53,7 +59,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored auction response" assert response.id == bidRequest.id @@ -79,7 +85,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored bid response" assert response.id == bidRequest.id @@ -108,7 +114,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored bid response and change bid.impId on imp.id" assert response.id == bidRequest.id @@ -137,7 +143,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain warning information" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] @@ -158,7 +164,7 @@ class StoredResponseSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid == [storedAuctionResponse] @@ -187,7 +193,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid == [storedAuctionResponse] @@ -211,7 +217,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid @@ -241,7 +247,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain warning information" assert response.ext?.warnings[ErrorType.PREBID]*.message.contains('SeatBid can\'t be null in stored response') @@ -249,4 +255,127 @@ class StoredResponseSpec extends BaseSpec { and: "PBS not send request to bidder" assert bidder.getRequestCount(bidRequest.id) == 0 } + + def "PBS should set seatBid in response from single imp.ext.prebid.storedBidResponse.seatbidobj when it is defined"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: storedAuctionResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty seatbid"() { + given: "Default basic BidRequest with empty stored response" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid()) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: Seat can\'t be empty in stored response seatBid' + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty bids"() { + given: "Default basic BidRequest with empty bids for stored response" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid(bid: [], seat: GENERIC)) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: There must be at least one bid in stored response seatBid' + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should prefer seatbidobj over storedAuctionResponse.id from imp when both are present"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + id = PBSUtils.randomString + seatBidObject = storedAuctionResponse + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should set seatBids in response from multiple imp.ext.prebid.storedBidResponse.seatbidobj when it is defined"() { + given: "BidRequest with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [impWithSeatBidObject, impWithSeatBidObject] + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response bids as requested" + assert convertToComparableSeatBid(response.seatbid).bid.flatten().sort() == + bidRequest.imp.ext.prebid.storedAuctionResponse.seatBidObject.bid.flatten().sort() + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should prefer seatbidarr from request over seatbidobj from imp when both are present"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.tap { + imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + seatBidObject = SeatBid.getStoredResponse(bidRequest) + } + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBids: [storedAuctionResponse]) + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert response.seatbid == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + private static final Imp getImpWithSeatBidObject() { + def imp = Imp.defaultImpression + def bids = Bid.getDefaultBids([imp]) + def seatBid = new SeatBid(bid: bids, seat: GENERIC) + imp.tap { + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: seatBid) + } + } + + private static final List convertToComparableSeatBid(List seatBid) { + seatBid*.tap { + it.bid*.ext = null + it.group = null + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy index 6005a232262..e24d22b4b8f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.PriceGranularityType import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse @@ -17,23 +18,29 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode import java.nio.charset.StandardCharsets +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.PriceGranularityType.UNKNOWN import static org.prebid.server.functional.model.response.auction.ErrorType.TARGETING import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class TargetingSpec extends BaseSpec { private static final Integer TARGETING_PARAM_NAME_MAX_LENGTH = 20 + private static final Integer TARGETING_KEYS_SIZE = 14 private static final Integer MAX_AMP_TARGETING_TRUNCATION_LENGTH = 11 private static final String DEFAULT_TARGETING_PREFIX = "hb" private static final Integer TARGETING_PREFIX_LENGTH = 11 private static final Integer MAX_TRUNCATE_ATTR_CHARS = 255 + private static final String HB_ENV_AMP = "amp" def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -668,7 +675,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain default targeting prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 + assert targeting.size() == TARGETING_KEYS_SIZE assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } @@ -694,7 +701,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 + assert targeting.size() == TARGETING_KEYS_SIZE assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } @@ -1100,6 +1107,249 @@ class TargetingSpec extends BaseSpec { assert ampData.secondUnknownField == secondUnknownValue } + def "PBS amp should always send hb_env=amp when stored request does not contain app"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default bid request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Amp response should contain amp hb_env" + def targeting = ampResponse.targeting + assert targeting["hb_env"] == HB_ENV_AMP + } + + def "PBS auction should throw error when price granularity from original request is empty"() { + given: "Default bidRequest with empty price granularity" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS auction should prioritize price granularity from original request over account config"() { + given: "Default bidRequest with price granularity" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity as PriceGranularityType) + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + } + + and: "Account in the DB" + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: PBSUtils.getRandomEnum(PriceGranularityType)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS amp should prioritize price granularity from original request over account config"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity) + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config with different name case when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from default account config when original request doesn't contain price granularity"() { + given: "Pbs with default account that include privacySandbox configuration" + def priceGranularity = PBSUtils.getRandomEnum(PriceGranularityType, [UNKNOWN]) + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def pbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(accountConfig)]) + + and: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + } + + def "PBS auction should include include default price granularity when original request and account config doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include default price granularity" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.default + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(priceGranularity: UNKNOWN)] + } + + def "PBS amp should throw error when price granularity from original request is empty"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest with empty price granularity" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS amp should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + setAccountId(ampRequest.account) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, priceGranularity) + accountDao.save(account) + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + return new Account(uuid: accountId, config: accountConfig) + } + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy new file mode 100644 index 00000000000..9ca353e0088 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy @@ -0,0 +1,1157 @@ +package org.prebid.server.functional.tests.module + +import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.config.AbTest +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.FetchStatus +import org.prebid.server.functional.model.request.auction.TraceLevel +import org.prebid.server.functional.model.response.auction.AnalyticResult +import org.prebid.server.functional.model.response.auction.InvocationResult +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.config.ModuleHookImplementation.RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.response.auction.InvocationStatus.INVOCATION_FAILURE +import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ModuleActivityName.AB_TESTING +import static org.prebid.server.functional.model.response.auction.ModuleActivityName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_INVOCATION + +class AbTestingModuleSpec extends ModuleBaseSpec { + + private final static String NO_INVOCATION_METRIC = "modules.module.%s.stage.%s.hook.%s.success.no-invocation" + private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call" + private final static String EXECUTION_ERROR_METRIC = "modules.module.%s.stage.%s.hook.%s.execution-error" + private final static Integer MIN_PERCENT_AB = 0 + private final static Integer MAX_PERCENT_AB = 100 + private final static String INVALID_HOOK_MESSAGE = "Hook implementation does not exist or disabled" + + private final static Map> ORTB_STAGES = [(BIDDER_REQUEST) : [ModuleName.ORTB2_BLOCKING], + (RAW_BIDDER_RESPONSE): [ModuleName.ORTB2_BLOCKING]] + private final static Map> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]] + private final static Map> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES + + private final static Map MULTI_MODULE_CONFIG = getResponseCorrectionConfig() + getOrtb2BlockingSettings() + + ['hooks.host-execution-plan': null] + + private final static PrebidServerService ortbModulePbsService = pbsServiceFactory.getService(getOrtb2BlockingSettings()) + private final static PrebidServerService pbsServiceWithMultipleModules = pbsServiceFactory.getService(MULTI_MODULE_CONFIG) + + def "PBS shouldn't apply a/b test config when config of ab test is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def abTest = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + enabled = false + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + it.abTests = [abTest] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + } + + def "PBS shouldn't apply valid a/b test config when module is disabled"() { + given: "PBS service with disabled module config" + def pbsConfig = getOrtb2BlockingSettings(false) + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [INVOCATION_FAILURE, INVOCATION_FAILURE] + it.action == [null, null] + it.analyticsTags == [null, null] + it.message == [INVALID_HOOK_MESSAGE, INVALID_HOOK_MESSAGE] + } + + and: "Metric for specified module should be with error call" + def metrics = prebidServerService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't apply a/b test config when module name is not matched"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(moduleName)] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + moduleName << [ModuleName.ORTB2_BLOCKING.code.toUpperCase(), PBSUtils.randomString] + } + + def "PBS should apply a/b test config for each module when multiple config are presents and set to allow modules"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for allowed to run ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + and: "Metric for allowed to run response-correction module should be updated based on ab test config" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + } + + def "PBS should apply a/b test config for each module when multiple config are presents and set to skip modules"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + and: "Metric for skipped response-correction module should be updated based on ab test config" + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + } + + def "PBS should apply a/b test config for each module when multiple config are presents with different percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for allowed to run response-correction module should be updated based on ab test config" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + } + + def "PBS should ignore accounts property for a/b test config when ab test config specialize for specific account"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + } + + def "PBS should apply a/b test config and run module when config is on max percentage or default value"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb module and run module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + percentActive | percentActiveSnakeCase + MAX_PERCENT_AB | null + null | MAX_PERCENT_AB + null | null + } + + def "PBS should apply a/b test config and skip module when config is on min percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb module and skip this module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + percentActive | percentActiveSnakeCase + MIN_PERCENT_AB | null + null | MIN_PERCENT_AB + } + + def "PBS shouldn't apply a/b test config without warnings and errors when percent config is out of lover range"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "No error or warning should be emitted" + assert !response.ext.errors + assert !response.ext.warnings + + and: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + percentActive | percentActiveSnakeCase + PBSUtils.randomNegativeNumber | null + null | PBSUtils.randomNegativeNumber + } + + def "PBS should apply a/b test config and run module without warnings and errors when percent config is out of appear range"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "No error or warning should be emitted" + assert !response.ext.errors + assert !response.ext.warnings + + and: "PBS should apply ab test config for ortb module and run it" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + percentActive | percentActiveSnakeCase + PBSUtils.getRandomNumber(MAX_PERCENT_AB) | null + null | PBSUtils.getRandomNumber(MAX_PERCENT_AB) + } + + def "PBS should include analytics tags when a/b test config when logAnalyticsTag is enabled or empty"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module without analytics tags" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + true | null + null | true + null | null + } + + def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is applied by percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + } + + and: "Shouldn't include any analytics tags" + assert !invocationResults?.analyticsTags?.any() + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + false | null + null | false + } + + def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is non-applied by percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MAX_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + false | null + null | false + } + + def "PBS shouldn't apply analytics tags for all module stages when module contain multiple stages"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = PBSUtils.getRandomNumber(MIN_PERCENT_AB, MAX_PERCENT_AB) + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for all stages of specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status.every { status -> status == it.status.first() } + it.action.every { action -> action == it.action.first() } + } + + and: "All resonances have same analytics" + def abTestingInvocationResults = (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name == AB_TESTING.value } + verifyAll(abTestingInvocationResults) { + it.status.flatten().every { status -> status == it.status.flatten().first() } + it.results.status.flatten().every { status -> status == it.results.status.flatten().first() } + it.results.values.module.flatten().every { module -> module == it.results.values.module.flatten().first() } + } + } + + def "PBS should apply a/b test config from host config when accounts is not specified when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, accouns).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + accouns << [null, []] + } + + def "PBS should apply a/b test config from host config for specific accounts and only specified module when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def accountId = PBSUtils.randomNumber + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber, accountId]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + setAccountId(accountId as String) + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should apply a/b test config from host config for specific account and general config when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def accountId = PBSUtils.randomNumber + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, []).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code, [accountId]).tap { + it.percentActive = MIN_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + setAccountId(accountId as String) + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + and: "Metric for skipped response-correction module should be updated based on ab test config" + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't apply a/b test config from host config for non specified accounts when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.activities.name.flatten() == [ORTB2_BLOCKING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SUCCESS_ALLOW].value + it.analyticsTags.activities.results.values.module.flatten().every { it == null } + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be as default call" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should prioritise a/b test config from default account and only specified module when host and default account contains a/b test configs"() { + given: "PBS service with specific ab test config" + def accountExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + }] + } + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan) + } + + def hostExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(hostExecutionPlan)] + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module and call it based on all execution plans" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION, NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING, AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other modules and call them based on all execution plans" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 2 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 2 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should prioritise a/b test config from account over default account and only specified module when specific account and default account contains a/b test configs"() { + given: "PBS service with specific ab test config" + def accountExecutionPlan = new ExecutionPlan(abTests: [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)]) + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan) + } + + def pbsConfig = MULTI_MODULE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + + and: "PBS should apply ab test config for specified module" + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + private static List filterInvocationResultsByModule(List invocationResults, ModuleName moduleName) { + invocationResults.findAll { it.hookId.moduleCode == moduleName.code } + } + + private static BidRequest getBidRequestWithTrace() { + defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy new file mode 100644 index 00000000000..82727b16a56 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy @@ -0,0 +1,532 @@ +package org.prebid.server.functional.tests.module + +import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AdminConfig +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingConfig +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.RichmediaFilter +import org.prebid.server.functional.model.request.auction.TraceLevel +import org.prebid.server.functional.model.response.auction.InvocationResult +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION + +class GeneralModuleSpec extends ModuleBaseSpec { + + private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call" + private final static String NOOP_METRIC = "modules.module.%s.stage.%s.hook.%s.success.noop" + + private final static Map DISABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'false'] + private final static Map ENABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'true'] + + private final static Map> ORTB_STAGES = [(BIDDER_REQUEST) : [ORTB2_BLOCKING], + (RAW_BIDDER_RESPONSE): [ORTB2_BLOCKING]] + private final static Map> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]] + private final static Map> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES + private final static Map MULTI_MODULE_CONFIG = getRichMediaFilterSettings(PBSUtils.randomString) + + getOrtb2BlockingSettings() + + ['hooks.host-execution-plan': encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES))] + + private final static PrebidServerService pbsServiceWithMultipleModule = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + DISABLED_INVOKE_CONFIG) + private final static PrebidServerService pbsServiceWithMultipleModuleWithRequireInvoke = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG) + + def "PBS should call all modules and traces response when account config is empty and require-config-to-invoke is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModule) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING, PB_RICHMEDIA_FILTER].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, pbResponseCorrection: pbResponseCorrectionConfig) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModule) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + pbRichmediaFilterConfig | pbResponseCorrectionConfig + new RichmediaFilter() | new PbResponseCorrection() + new RichmediaFilter() | new PbResponseCorrection(enabled: false) + new RichmediaFilter() | new PbResponseCorrection(enabled: true) + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection() + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true) + } + + def "PBS should call all modules and traces response when default-account includes modules config and require-config-to-invoke is enabled"() { + given: "PBS service with module config" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter(), ortb2Blocking: new Ortb2BlockingConfig()) + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(modules: pbsModulesConfig) + } + + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account with enabled response correction module" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, ortb2Blocking: ortb2BlockingConfig) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + pbRichmediaFilterConfig | ortb2BlockingConfig + new RichmediaFilter() | new Ortb2BlockingConfig() + new RichmediaFilter() | new Ortb2BlockingConfig(attributes: [:] as Map) + new RichmediaFilter() | new Ortb2BlockingConfig(attributes: [:] as Map) + new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig() + new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig(attributes: [:] as Map) + } + + def "PBS should call specified module and traces response when account config includes that module and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account with enabled response correction module" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter()))) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called module" + def invocationTrace = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RICHMEDIA_FILTER.code }) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + } + + def "PBS shouldn't call any modules and traces that in response when account config is empty and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + def "PBS should call all modules without account config when modules enabled in module-execution host config"() { + given: "PBS service with module-execution config" + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'true'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call any module without account config when modules disabled in module-execution host config"() { + given: "PBS service with module-execution config" + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call module and not override host config when default-account module-execution config enabled module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should call module without account module config when default-account module-execution config enabling module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call any modules without account config when default-account module-execution config not enabling module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): moduleExecutionStatus])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + moduleExecutionStatus << [false, null] + } + + def "PBS should prioritize specific account module-execution config over default-account module-execution config when both are present"() { + given: "PBS service with default account config" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): false])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true]))) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 4a342e602a4..453de43aa3c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -2,12 +2,16 @@ package org.prebid.server.functional.tests.module import org.prebid.server.functional.model.config.Endpoint import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Stage import org.prebid.server.functional.tests.BaseSpec import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_REQUEST_CORRECTION +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST class ModuleBaseSpec extends BaseSpec { @@ -22,14 +26,21 @@ class ModuleBaseSpec extends BaseSpec { repository.removeAllDatabaseData() } + protected static Map getResponseCorrectionConfig(Endpoint endpoint = OPENRTB2_AUCTION) { + ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled": true, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getRichMediaFilterSettings(String scriptPattern, - boolean filterMraidEnabled = true, + Boolean filterMraidEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { ["hooks.${PB_RICHMEDIA_FILTER.code}.enabled" : true, "hooks.modules.${PB_RICHMEDIA_FILTER.code}.mraid-script-pattern": scriptPattern, "hooks.modules.${PB_RICHMEDIA_FILTER.code}.filter-mraid" : filterMraidEnabled, - "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]))] + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]]))] + .findAll { it.value != null } .collectEntries { key, value -> [(key.toString()): value.toString()] } } @@ -44,4 +55,9 @@ class ModuleBaseSpec extends BaseSpec { protected static Map getOrtb2BlockingSettings(boolean isEnabled = true) { ["hooks.${ORTB2_BLOCKING.code}.enabled": isEnabled as String] } + + protected static Map getRequestCorrectionSettings(Endpoint endpoint = OPENRTB2_AUCTION, Stage stage = PROCESSED_AUCTION_REQUEST) { + ["hooks.${PB_REQUEST_CORRECTION.code}.enabled": "true", + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_REQUEST_CORRECTION, [stage]))] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy similarity index 99% rename from src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy rename to src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy index 511c2101eee..8a99628b70c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy @@ -1,4 +1,4 @@ -package org.prebid.server.functional.tests.module +package org.prebid.server.functional.tests.module.analyticstag import org.prebid.server.functional.model.config.AccountAnalyticsConfig import org.prebid.server.functional.model.config.AccountConfig @@ -16,6 +16,7 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ModuleActivityName import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy new file mode 100644 index 00000000000..bb644508090 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -0,0 +1,1642 @@ +package org.prebid.server.functional.tests.module.ortb2blocking + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingActionOverride +import org.prebid.server.functional.model.config.Ortb2BlockingAttributeConfig +import org.prebid.server.functional.model.config.Ortb2BlockingAttribute +import org.prebid.server.functional.model.config.Ortb2BlockingConditions +import org.prebid.server.functional.model.config.Ortb2BlockingConfig +import org.prebid.server.functional.model.config.Ortb2BlockingOverride +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.Asset +import org.prebid.server.functional.model.request.auction.Audio +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidderControls +import org.prebid.server.functional.model.request.auction.GenericPreferredBidder +import org.prebid.server.functional.model.request.auction.Ix +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Video +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidMediaType +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.IX +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR +import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class Ortb2BlockingSpec extends ModuleBaseSpec { + + private static final String WILDCARD = '*' + private static final Map IX_CONFIG = ["adapters.ix.enabled" : "true", + "adapters.ix.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + IX_CONFIG + + ['adapter-defaults.ortb.multiformat-supported': 'false']) + + def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS should be able to send original array ortb2 attribute to bidder alias"() { + given: "Default bid request with alias" + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + ext.prebid.aliases = [(ALIAS.value): GENERIC] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.alias = new Generic() + } + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [ortb2Attributes]*.toString() + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when bid request imps type doesn't match with attribute type"() { + given: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain single media type" + assert bidderRequest.imp.first.mediaTypes.size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + bidRequest | attributeName + BidRequest.defaultVideoRequest | BANNER_BATTR + BidRequest.defaultAudioRequest | VIDEO_BATTR + BidRequest.defaultBidRequest | AUDIO_BATTR + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when preferredMediaType doesn't match with attribute type"() { + given: "Default bid request with multiply types" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.banner = Banner.defaultBanner + imp.first.video = Video.defaultVideo + imp.first.audio = Audio.defaultAudio + ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: preferredMediaType)) + } + + and: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain only preferred media type" + assert bidderRequest.imp.first.mediaTypes == [preferredMediaType] + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + preferredMediaType | attributeName + VIDEO | BANNER_BATTR + AUDIO | VIDEO_BATTR + BANNER | AUDIO_BATTR + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when account level preferredMediaType doesn't match with attribute type"() { + given: "Default bid request with multiply types" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.banner = Banner.defaultBanner + imp.first.video = Video.defaultVideo + imp.first.audio = Audio.defaultAudio + } + + and: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName).tap { + config.auction = new AccountAuctionConfig(preferredMediaType: [(GENERIC): preferredMediaType]) + } + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain only preferred media type" + assert bidderRequest.imp.first.mediaTypes == [preferredMediaType] + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + preferredMediaType | attributeName + VIDEO | BANNER_BATTR + AUDIO | VIDEO_BATTR + BANNER | AUDIO_BATTR + } + + def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, ortb2Attributes, attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.prebid.modules.errors.ortb2Blocking["ortb2-blocking-bidder-request"].first + .contains("field in account configuration is not an array") + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request shouldn't contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !getOrtb2Attributes(bidderRequest, attributeName) + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.prebid.modules.errors.ortb2Blocking["ortb2-blocking-bidder-request"].first + .contains("field in account configuration has unexpected type") + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request shouldn't contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !getOrtb2Attributes(bidderRequest, attributeName) + + where: + ortb2Attributes | attributeName + PBSUtils.randomNumber | BADV + PBSUtils.randomNumber | BAPP + PBSUtils.randomNumber | BCAT + PBSUtils.randomString | BANNER_BATTR + PBSUtils.randomString | VIDEO_BATTR + PBSUtils.randomString | AUDIO_BATTR + PBSUtils.randomString | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should send only not matched ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName), + getBidWithOrtb2Attribute(bidRequest.imp.first, allowedOrtb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + allowedOrtb2Attributes | disallowedOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | BANNER_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | VIDEO_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | AUDIO_BATTR + } + + def "PBS should left only not matched ortb2 attribute to bidder with multiply type imp when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + banner = Banner.getDefaultBanner().tap { + battr = [PBSUtils.randomNumber] + } + video = Video.getDefaultVideo().tap { + battr = [PBSUtils.randomNumber] + } + audio = Audio.getDefaultAudio().tap { + battr = [PBSUtils.randomNumber] + } + ext.prebid.bidder.generic = null + ext.prebid.bidder.ix = Ix.default + } + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def disallowedOrtb2Attributes = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def removeBid = getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName).tap { + it.mediaType = enforceType + } + def presentBid = getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName).tap { + it.mediaType = presentType + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [removeBid, presentBid] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.mediaType == presentType + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [disallowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + attributeName | enforceType | presentType + BANNER_BATTR | BidMediaType.BANNER | BidMediaType.AUDIO + VIDEO_BATTR | BidMediaType.VIDEO | BidMediaType.BANNER + AUDIO_BATTR | BidMediaType.AUDIO | BidMediaType.VIDEO + } + + def "PBS should send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = false + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain proper seatbid" + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should discard unknown adomain bids when enforcement is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def allowedOrtb2Attributes = PBSUtils.randomString + def bidPrice = PBSUtils.randomPrice + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + price = bidPrice + 1 // to guarantee higher priority by default settings + } + def bidWithAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = [allowedOrtb2Attributes] + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain, bidWithAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, BADV) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should not discard unknown adomain bids when enforcement is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2BlockingAttributeConfig << [new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: false), + new Ortb2BlockingAttributeConfig(enforceBlocks: false, blockUnknownAdomain: true), + new Ortb2BlockingAttributeConfig(enforceBlocks: true)] + } + + def "PBS should discard unknown adv cat bids when enforcement is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def allowedOrtb2Attributes = PBSUtils.randomString + def bidPrice = PBSUtils.randomPrice + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + price = bidPrice + 1 // to guarantee higher priority by default settings + } + def bidWithAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = [allowedOrtb2Attributes] + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain, bidWithAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, BCAT) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should not discard unknown adv cat bids when enforcement is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2BlockingAttributeConfig << [new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: false), + new Ortb2BlockingAttributeConfig(enforceBlocks: false, blockUnknownAdvCat: true), + new Ortb2BlockingAttributeConfig(enforceBlocks: true)] + } + + def "PBS should not discard bids with deals when allowed ortb2 attribute for deals is matched"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def attributes = [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2Attributes]).tap { + enforceBlocks = true + }] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = PBSUtils.randomNumber }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should discard bids with deals when allowed ortb2 attribute for deals is not matched"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def attributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([allowedOrtb2Attributes, dielsOrtb2Attributes], attributeName, [allowedOrtb2Attributes]).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): attributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, dielsOrtb2Attributes, attributeName) + .tap { dealid = PBSUtils.randomNumber }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid.bid.flatten().size() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + allowedOrtb2Attributes | dielsOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override enforcement by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC), + new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: IX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only openx seatbid" + assert response.seatbid.size() == 1 + assert response.seatbid.first.seat == IX + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override enforcement by media type"() { + given: "Bid request with multy type imp" + def bannerImp = Imp.getDefaultImpression(BANNER) + def videoImp = Imp.getDefaultImpression(VIDEO) + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + imp = [bannerImp, videoImp] + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bannerImp, ortb2Attributes, attributeName)]), + new SeatBid(bid: [getBidWithOrtb2Attribute(videoImp, ortb2Attributes, attributeName)])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.impid == bannerImp.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + } + + def "PBS should be able to override enforcement by media type for battr attribute"() { + given: "Default bid request with proper ortb attribute" + BidRequest bidRequest = getBidRequestForOrtbAttribute(attributeName, [PBSUtils.randomNumber]).tap { +// default resolve for bids always prefer type from request, ix from response and only then from request if null + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [mediaType]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bid = getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName).tap { + it.mediaType = bidMediaType + it.adm = new Adm(assets: [Asset.defaultAsset]) // required for video type + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bid])] + + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.impid == bidRequest.imp.first.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attribute]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + attributeName | mediaType | bidMediaType + BANNER_BATTR | BANNER | null + VIDEO_BATTR | VIDEO | null + AUDIO_BATTR | AUDIO | null + BANNER_BATTR | BANNER | BidMediaType.BANNER + VIDEO_BATTR | VIDEO | BidMediaType.VIDEO + AUDIO_BATTR | AUDIO | BidMediaType.AUDIO + BANNER_BATTR | BANNER | BidMediaType.AUDIO + VIDEO_BATTR | VIDEO | BidMediaType.BANNER + AUDIO_BATTR | AUDIO | BidMediaType.VIDEO + } + + def "PBS shouldn't be able to override enforcement by incorrect media type for battr attribute"() { + given: "Default bid request with proper ortb attribute" + BidRequest bidRequest = getBidRequestForOrtbAttribute(attributeName, [PBSUtils.randomNumber]).tap { + // default resolve for bids always prefer type from request, ix from response and only then from request if null + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [mediaType]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bid = getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName).tap { + it.mediaType = bidMediaType + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bid])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid.bid.flatten().size() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request should contain original ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == getOrtb2Attributes(bidRequest, attributeName) + + where: + attributeName | mediaType | bidMediaType + BANNER_BATTR | AUDIO | null + VIDEO_BATTR | BANNER | null + AUDIO_BATTR | VIDEO | null + } + + def "PBS should be able to override enforcement by deal id"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: new Ortb2BlockingConditions(dealIds: [dealId.toString()])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2AttributesForDeals]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [blockingCondition]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + dealId | ortb2Attributes | ortb2AttributesForDeals | attributeName + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BADV + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BAPP + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BCAT + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [GENERIC]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [overrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [ortb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [overrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | overrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [overrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [ortb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [overrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | overrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + } + + def "PBS should be able to override block unknown adomain by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV).tap { + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdomain: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutAdomain], seat: GENERIC), + new SeatBid(bid: [bidWithOutAdomain], seat: IX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only ix seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.seat == IX + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adomain by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdomain: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutAdomain])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adv-cat by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT).tap { + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdvCat: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutCat = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutCat], seat: GENERIC), + new SeatBid(bid: [bidWithOutCat], seat: IX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only ix seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.seat == IX + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adv-cat by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdvCat: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutCat = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutCat])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override allowed ortb2 attribute for deals by deal ids"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def dealId = PBSUtils.randomNumber + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [dealOverrideAttributes]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [ortb2BlockingOverride]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | dealOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should use first override when multiple match same condition"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: blockingCondition) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [firstOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response should contain proper warning" + assert response?.ext?.prebid?.modules?.warnings?.ortb2Blocking["ortb2-blocking-bidder-request"] == + ["More than one conditions matches request. Bidder: generic, request media types: [${bidRequest.imp[0].mediaTypes[0].value}]"] + + where: + blockingCondition | ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + new Ortb2BlockingConditions(mediaType: [VIDEO]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + new Ortb2BlockingConditions(mediaType: [AUDIO]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [BidderName.WILDCARD])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [GENERIC])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [secondOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [MediaType.WILDCARD])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [bidRequest.imp[0].mediaTypes[0]])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [secondOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should merge allowed bundle for deals overrides together"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def dealId = PBSUtils.randomNumber + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [ortb2Attributes.last], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig(ortb2Attributes, attributeName, [ortb2Attributes.first]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [ortb2BlockingOverride]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == ortb2Attributes*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + [PBSUtils.randomString, PBSUtils.randomString] | BADV + [PBSUtils.randomString, PBSUtils.randomString] | BCAT + [PBSUtils.randomNumber, PBSUtils.randomNumber] | BANNER_BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | VIDEO_BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | AUDIO_BATTR + } + + def "PBS should not be override from config when ortb2 attribute present in incoming request"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName, bidRequestAttribute) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain original ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == getOrtb2Attributes(bidRequest, attributeName) + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + bidRequestAttribute | ortb2Attributes | attributeName + [PBSUtils.randomString] | PBSUtils.randomString | BADV + [PBSUtils.randomString] | PBSUtils.randomString | BAPP + [PBSUtils.randomString] | PBSUtils.randomString | BCAT + [PBSUtils.randomNumber] | PBSUtils.randomNumber | BANNER_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | VIDEO_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | AUDIO_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | BTYPE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with rejected advertiser blocked status code"() { + given: "Default bidRequest with returnAllBidStatus attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV).tap { + it.ext.prebid.returnAllBidStatus = true + } + + and: "Default bidder response with aDomain" + def aDomain = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, aDomain, BADV)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with blocking configuration" + def attributes = [(BADV): new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockedAdomain: [aDomain])] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) + accountDao.save(account) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == ErrorType.GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED + } + + private static Account getAccountWithOrtb2BlockingConfig(String accountId, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { + getAccountWithOrtb2BlockingConfig(accountId, [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig(ortb2Attributes, attributeName)]) + } + + private static Account getAccountWithOrtb2BlockingConfig(String accountId, Map attributes) { + def blockingConfig = new Ortb2BlockingConfig(attributes: attributes) + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [BIDDER_REQUEST, RAW_BIDDER_RESPONSE]) + def moduleConfig = new PbsModulesConfig(ortb2Blocking: blockingConfig) + def accountHooksConfig = new AccountHooksConfiguration(executionPlan: executionPlan, modules: moduleConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: accountId, config: accountConfig) + } + + private static BidRequest getBidRequestForOrtbAttribute(Ortb2BlockingAttribute attribute, List attributeValue = null) { + switch (attribute) { + case BADV: + return BidRequest.defaultBidRequest.tap { + badv = attributeValue as List + } + case BAPP: + return BidRequest.defaultBidRequest.tap { + bapp = attributeValue as List + } + case BANNER_BATTR: + return BidRequest.defaultBidRequest.tap { + imp[0].banner.battr = attributeValue as List + } + case VIDEO_BATTR: + return BidRequest.defaultVideoRequest.tap { + imp[0].video.battr = attributeValue as List + } + case AUDIO_BATTR: + return BidRequest.defaultAudioRequest.tap { + imp[0].audio.battr = attributeValue as List + } + case BCAT: + return BidRequest.defaultBidRequest.tap { + bcat = attributeValue as List + } + case BTYPE: + return BidRequest.defaultBidRequest.tap { + imp[0].banner.btype = attributeValue as List + } + default: + throw new IllegalArgumentException("Unknown ortb2 attribute: $attribute") + } + } + + private static Bid getBidWithOrtb2Attribute(Imp imp, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { + Bid.getDefaultBid(imp).tap { + switch (attributeName) { + case BADV: + adomain = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BAPP: + bundle = (ortb2Attributes instanceof List) ? ortb2Attributes.first : ortb2Attributes + break + case BANNER_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case VIDEO_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case AUDIO_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BCAT: + cat = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BTYPE: + break + default: + throw new IllegalArgumentException("Unknown ortb2 attribute: $attributeName") + } + } + } + + private static List getOrtb2Attributes(BidRequest bidRequest, Ortb2BlockingAttribute attributeName) { + switch (attributeName) { + case BADV: + return bidRequest.badv + case BAPP: + return bidRequest.bapp + case BANNER_BATTR: + return bidRequest.imp[0].banner.battr*.toString() + case VIDEO_BATTR: + return bidRequest.imp[0].video.battr*.toString() + case AUDIO_BATTR: + return bidRequest.imp[0].audio.battr*.toString() + case BCAT: + return bidRequest.bcat + case BTYPE: + return bidRequest.imp[0].banner.btype*.toString() + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } + + private static List getOrtb2Attributes(Bid bid, Ortb2BlockingAttribute attributeName) { + switch (attributeName) { + case BADV: + return bid.adomain + case BAPP: + return [bid.bundle] + case BANNER_BATTR: + return bid.attr*.toString() + case VIDEO_BATTR: + return bid.attr*.toString() + case AUDIO_BATTR: + return bid.attr*.toString() + case BCAT: + return bid.cat + case BTYPE: + return null + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy new file mode 100644 index 00000000000..68b00bdd0d1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy @@ -0,0 +1,454 @@ +package org.prebid.server.functional.tests.module.pbrequestcorrection + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.PbRequestCorrectionConfig +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.AppExt +import org.prebid.server.functional.model.request.auction.AppPrebid +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.OperationState +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.OperationState.YES + +class PbRequestCorrectionSpec extends ModuleBaseSpec { + + private static final String PREBID_MOBILE = "prebid-mobile" + private static final String DEVICE_PREBID_MOBILE_PATTERN = "PrebidMobile/" + private static final String ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.1.5") + private static final String ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.2.3") + private static final String ANDROID = "android" + private static final String IOS = "IOS" + + private PrebidServerService pbsServiceWithRequestCorrectionModule = pbsServiceFactory.getService(requestCorrectionSettings) + + def "PBS should remove positive instl from imps for android app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp = imps + app.bundle = PBSUtils.getRandomCase(bundle) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl.every { it == null } + + where: + imps | bundle | requestCorrectionConfig + [Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + } + + def "PBS shouldn't remove negative instl from imps for android app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp = imps + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + imps << [[Imp.defaultImpression.tap { instl = OperationState.NO }], + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = OperationState.NO }], + [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = null }], + [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = OperationState.NO }]] + } + + def "PBS shouldn't remove positive instl from imps for not android or not prebid-mobile app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(source), version: PBSUtils.getRandomVersion(ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(bundle) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + bundle | source + IOS | PREBID_MOBILE + PBSUtils.randomString | PREBID_MOBILE + ANDROID | PBSUtils.randomString + ANDROID | PBSUtils.randomString + PREBID_MOBILE + ANDROID | PREBID_MOBILE + PBSUtils.randomString + } + + def "PBS shouldn't remove positive instl from imps for app when request correction is enabled for account but some required parameter is empty"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: source, version: version) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = instl + app.bundle = bundle + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + bundle | source | version | instl + null | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES + ANDROID | null | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES + ANDROID | PREBID_MOBILE | null | YES + ANDROID | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | null + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is threshold"() { + given: "Android APP bid request with version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.2.3") + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is higher then threshold"() { + given: "Android APP bid request with version higher then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.2.4")) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is disabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithInterstitial(interstitialCorrectionEnabled, enabled) + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + enabled | interstitialCorrectionEnabled + false | true + null | true + true | false + true | null + null | null + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is not applied for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig())) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS should remove pattern device.ua when request correction is enabled for account and user agent correction enabled"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.device.ua + + where: + deviceUa | requestCorrectionConfig + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true) + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true) + } + + def "PBS should remove only pattern device.ua when request correction is enabled for account and user agent correction enabled"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua.contains(deviceUa.replaceAll("PrebidMobile/[0-9][^ ]*", '').trim()) + + where: + deviceUa << ["${PBSUtils.randomNumber} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}", + "${PBSUtils.randomString} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString} ${PBSUtils.randomString}", + "${DEVICE_PREBID_MOBILE_PATTERN}", + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}", + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}" + ] + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and user agent correction disabled"() { + given: "Android APP bid request with version lover then version threshold" + def deviceUserAgent = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUserAgent) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithUserAgentCorrection(userAgentCorrectionEnabled, enabled) + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == deviceUserAgent + + where: + enabled | userAgentCorrectionEnabled + false | true + null | true + true | false + true | null + null | null + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and source not a prebid-mobile"() { + given: "Android APP bid request with version lover then version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: source, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + + where: + source << ["prebid", + "mobile", + PREBID_MOBILE + PBSUtils.randomString, + PBSUtils.randomString + PREBID_MOBILE, + "mobile-prebid", + PBSUtils.randomString] + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version biggest that threshold"() { + given: "Android APP bid request with version higher then version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6"))) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version threshold"() { + given: "Android APP bid request with version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.1.6")) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove device.ua pattern when request correction is enabled for account and version threshold"() { + given: "Android APP bid request with version higher then version threshold" + def randomDeviceUa = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6"))) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove device.ua pattern from device for android app when request correction is not applied for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def deviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig())) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain request device ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == deviceUa + } + + private static Account createAccountWithRequestCorrectionConfig(BidRequest bidRequest, + PbRequestCorrectionConfig requestCorrectionConfig) { + def pbsModulesConfig = new PbsModulesConfig(pbRequestCorrection: requestCorrectionConfig) + def accountHooksConfig = new AccountHooksConfiguration(modules: pbsModulesConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: bidRequest.accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy new file mode 100644 index 00000000000..c848c30e2e4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy @@ -0,0 +1,581 @@ +package org.prebid.server.functional.tests.module.responsecorrenction + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AppVideoHtml +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.Meta +import org.prebid.server.functional.model.response.auction.Prebid +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultVideoRequest +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class ResponseCorrectionSpec extends ModuleBaseSpec { + + private final PrebidServerService pbsServiceWithResponseCorrectionModule = pbsServiceFactory.getService( + ["adapter-defaults.modifying-vast-xml-allowed": "false", + "adapters.generic.modifying-vast-xml-allowed": "false"] + + responseCorrectionConfig) + + private final static int OPTIMAL_MAX_LENGTH = 20 + + def "PBS shouldn't modify response when in account correction module disabled"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest, responseCorrectionEnabled, appVideoHtmlEnabled) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + responseCorrectionEnabled | appVideoHtmlEnabled + false | true + true | false + false | false + } + + def "PBS shouldn't modify response with adm obj when request includes #distributionChannel distribution channel"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request video imp" + def bidRequest = getDefaultVideoRequest(distributionChannel) + + and: "Set bidder response with adm obj" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + distributionChannel << [SITE, DOOH] + } + + def "PBS shouldn't modify response for excluded bidder when bidder specified in config"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module and excluded bidders" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest).tap { + config.hooks.modules.pbResponseCorrection.appVideoHtml.excludedBidders = [GENERIC] + } + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response and emit warning when requested video impression respond with adm without VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response without adm obj when request includes #mediaType media type"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [mediaType] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType << [BANNER, AUDIO] + } + + def "PBS shouldn't modify response and emit logs when requested impression with native and adm value is asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + assert responseCorrection.size() == 1 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [NATIVE] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with empty adm"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(null) + seatbid[0].bid[0].nurl = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + admValue << [ + "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}", + "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}>", + "${PBSUtils.randomString}${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}>", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}>", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}>" + ] + } + + def "PBS should modify response when requested video impression respond with invalid adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + admValue << [ + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST>", + "<${PBSUtils.randomString}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + ] + } + + def "PBS should modify response when requested #mediaType impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType | admValue + BANNER | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + BANNER | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + BANNER | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + AUDIO | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + AUDIO | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + AUDIO | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + NATIVE | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + NATIVE | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + NATIVE | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + } + + def "PBS shouldn't modify response meta.mediaType to video and emit logs when requested impression with video and adm obj with asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and audio imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + } + + and: "Response should contain seatBib" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS should modify meta.mediaType and type for original response and also emit logs when response contains native meta.mediaType and adm without asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + seatbid[0].bid[0].ext = new BidExt(prebid: new Prebid(meta: new Meta(mediaType: NATIVE))) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 2 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has a JSON ADM, but without assets" as String) + } + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should media type for prebid meta" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + private static Account accountConfigWithResponseCorrectionModule(BidRequest bidRequest, Boolean enabledResponseCorrection = true, Boolean enabledAppVideoHtml = true) { + def modulesConfig = new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection( + enabled: enabledResponseCorrection, appVideoHtml: new AppVideoHtml(enabled: enabledAppVideoHtml))) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy index c14acd7d95d..c49743b275b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy @@ -12,11 +12,11 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.AnalyticResult import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION @@ -30,14 +30,61 @@ class RichMediaFilterSpec extends ModuleBaseSpec { private final PrebidServerService pbsServiceWithEnabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME)) private final PrebidServerService pbsServiceWithEnabledMediaFilterAndDifferentCaseStrategy = pbsServiceFactory.getService( (getRichMediaFilterSettings(PATTERN_NAME) + ["hooks.host-execution-plan": encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]).tap { - endpoints.values().first().stages.values().first().groups.first.hookSequenceSnakeCase = [new HookId(moduleCodeSnakeCase: PB_RICHMEDIA_FILTER.code, hookImplCodeSnakeCase: "${PB_RICHMEDIA_FILTER.code}-${ALL_PROCESSED_BID_RESPONSES.value}-hook")]})]) + endpoints.values().first().stages.values().first().groups.first.hookSequenceSnakeCase = [new HookId(moduleCodeSnakeCase: PB_RICHMEDIA_FILTER.code, hookImplCodeSnakeCase: "${PB_RICHMEDIA_FILTER.code}-${ALL_PROCESSED_BID_RESPONSES.value}-hook")] + })]) .collectEntries { key, value -> [(key.toString()): value.toString()] }) private final PrebidServerService pbsServiceWithDisabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME, false)) + def "PBS should process request without rich media module when host config have empty settings"() { + given: "Prebid server with empty settings for module" + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "BidRequest with stored response" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true + it.ext.prebid.trace = VERBOSE + it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = PBSUtils.randomString + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.getAccountId()) + accountDao.save(account) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "Response header should contain seatbid" + assert response.seatbid.size() == 1 + + and: "Response shouldn't contain errors of invalid creation" + assert !response.ext.errors + + and: "Response shouldn't contain analytics" + assert !getAnalyticResults(response) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + pbsConfig << [getRichMediaFilterSettings(PBSUtils.randomString, null), + getRichMediaFilterSettings(null, true), + getRichMediaFilterSettings(null, false), + getRichMediaFilterSettings(null, null)] + } + def "PBS should process request without analytics when adm matches with pattern name and filter set to disabled in host config"() { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -73,6 +120,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -95,10 +143,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -114,6 +164,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -151,6 +202,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -175,10 +227,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -194,6 +248,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -231,6 +286,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -255,10 +311,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -271,6 +329,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -308,6 +367,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { and: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -345,6 +405,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -367,10 +428,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index 71c7621eb20..fba9a5b44d5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -25,6 +25,7 @@ import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer @@ -36,14 +37,14 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { public static final Map FLOORS_CONFIG = ["price-floors.enabled" : "true", "settings.default-account-config": encode(defaultAccountConfigSettings)] - protected static final String basicFetchUrl = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT - protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer) + protected static final String BASIC_FETCH_URL = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT protected static final int MAX_MODEL_WEIGHT = 100 private static final int DEFAULT_MODEL_WEIGHT = 1 private static final int CURRENCY_CONVERSION_PRECISION = 3 private static final int FLOOR_VALUE_PRECISION = 4 + protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer) protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG) def setupSpec() { @@ -68,7 +69,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { protected static Account getAccountWithEnabledFetch(String accountId) { def priceFloors = new AccountPriceFloorsConfig(enabled: true, - fetch: new PriceFloorsFetch(url: basicFetchUrl + accountId, enabled: true)) + fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + accountId, enabled: true)) def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(priceFloors: priceFloors)) new Account(uuid: accountId, config: accountConfig) } @@ -85,7 +86,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { static BidRequest getStoredRequestWithFloors(DistributionChannel channel = SITE) { channel == SITE ? BidRequest.defaultStoredRequest.tap { ext.prebid.floors = ExtPrebidFloors.extPrebidFloors } - : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: 1, floors: ExtPrebidFloors.extPrebidFloors))) + : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: ENABLED, floors: ExtPrebidFloors.extPrebidFloors))) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index 581a71644a5..786a70a6b86 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -243,7 +243,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = priceFloors config.auction.priceFloorsSnakeCase = new AccountPriceFloorsConfig(enabled: true, - fetch: new PriceFloorsFetch(url: basicFetchUrl + bidRequest.accountId, enabled: priceFloorsSnakeCase)) + fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + bidRequest.accountId, enabled: priceFloorsSnakeCase)) } accountDao.save(account) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy index 2ae431c1530..4d47947670a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy @@ -869,7 +869,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Bid response with 2 bids: bid.price = floorValue, dealBid.price < floorValue" def dealBidPrice = floorValue - 0.1 diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 8316ee54233..f282d69f600 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -17,28 +17,31 @@ import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH +import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Location.REQUEST import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { - private static final int MAX_ENFORCE_FLOORS_RATE = 100 + private static final int ENFORCE_FLOORS_RATE_MAX = 100 private static final int DEFAULT_MAX_AGE_SEC = 600 private static final int DEFAULT_PERIOD_SEC = 300 - private static final int MIN_TIMEOUT_MS = 10 - private static final int MAX_TIMEOUT_MS = 10000 - private static final int MIN_SKIP_RATE = 0 - private static final int MAX_SKIP_RATE = 100 - private static final int MIN_DEFAULT_FLOOR_VALUE = 0 - private static final int MIN_FLOOR_MIN = 0 + private static final int TIMEOUT_MS_MIN = 10 + private static final int TIMEOUT_MS_MAX = 10000 + private static final int SKIP_RATE_MIN = 0 + private static final int SKIP_RATE_MAX = 100 + private static final int USE_FETCH_DATA_RATE_MIN = 0 + private static final int USE_FETCH_DATA_RATE_MAX = 100 + private static final int DEFAULT_FLOOR_VALUE_MIN = 0 + private static final int FLOOR_MIN = 0 private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure" @@ -51,7 +54,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch and fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "PBS fetch rules from floors provider" @@ -61,7 +64,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS should signal bids" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -76,7 +79,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch and fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.enabled = accountConfigEnabled } accountDao.save(account) @@ -88,7 +91,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should no fetching, no signaling, no enforcing" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 0 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 0 def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert !bidderRequest.imp[0].bidFloor @@ -106,7 +109,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, without fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.url = null } accountDao.save(account) @@ -116,9 +119,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "PBS should log error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, bidRequest.site.publisher.id) + def floorsLogs = getLogsByText(logs, bidRequest.accountId) assert floorsLogs.size() == 1 - assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.site.publisher.id") + assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.accountId") and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() @@ -133,8 +136,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxAgeSec in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch = fetchConfig(bidRequest.app.publisher.id, DEFAULT_MAX_AGE_SEC - 1) + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch = fetchConfig(bidRequest.accountId, DEFAULT_MAX_AGE_SEC - 1) } accountDao.save(account) @@ -143,14 +146,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() where: - fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSec: max) }, - { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSecSnakeCase: max) }] + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSec: max) }, + { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSecSnakeCase: max) }] } def "PBS should validate fetch.period-sec from account config"() { @@ -158,7 +161,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, periodSec in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch = fetchConfig(DEFAULT_PERIOD_SEC, defaultAccountConfigSettings.auction.priceFloors.fetch.maxAgeSec) } @@ -169,7 +172,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -186,7 +189,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxFileSizeKb in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.maxFileSizeKb = PBSUtils.randomNegativeNumber } accountDao.save(account) @@ -196,7 +199,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -207,7 +210,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxRules in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.maxRules = PBSUtils.randomNegativeNumber } accountDao.save(account) @@ -217,7 +220,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -228,8 +231,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, timeoutMs in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch = fetchConfig(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS) + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch = fetchConfig(TIMEOUT_MS_MIN, TIMEOUT_MS_MAX) } accountDao.save(account) @@ -238,7 +241,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -255,7 +258,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, enforceFloorsRate in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.tap { it.enforceFloorsRate = enforceFloorsRate it.enforceFloorsRateSnakeCase = enforceFloorsRateSnakeCase @@ -268,7 +271,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -276,9 +279,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { where: enforceFloorsRate | enforceFloorsRateSnakeCase PBSUtils.randomNegativeNumber | null - MAX_ENFORCE_FLOORS_RATE + 1 | null + ENFORCE_FLOORS_RATE_MAX + 1 | null null | PBSUtils.randomNegativeNumber - null | MAX_ENFORCE_FLOORS_RATE + 1 + null | ENFORCE_FLOORS_RATE_MAX + 1 } def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() { @@ -288,7 +291,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = true } accountDao.save(account) @@ -297,7 +300,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 } def "PBS should process floors from request when price-floors.fetch.enabled = false in account config"() { @@ -305,7 +308,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with fetch.enabled, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch = fetch } accountDao.save(account) @@ -320,17 +323,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "PBS should not fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 0 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor where: - fetch << [new PriceFloorsFetch(enabled: false, url: basicFetchUrl), new PriceFloorsFetch(url: basicFetchUrl)] + fetch << [new PriceFloorsFetch(enabled: false, url: BASIC_FETCH_URL), new PriceFloorsFetch(url: BASIC_FETCH_URL)] } - def "PBS should fetch data from provider when use-dynamic-data = true"() { + def "PBS should fetch data from provider when use-dynamic-data enabled"() { given: "Pbs with PF configuration with useDynamicData" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { auction.priceFloors.tap { @@ -347,7 +350,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.useDynamicData = accountUseDynamicData config.auction.priceFloors.useDynamicDataSnakeCase = accountUseDynamicDataSnakeCase } @@ -358,13 +361,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS cache rules and processes auction request" cacheFloorsProviderRules(bidRequest, floorValue, pbsService) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -382,6 +385,211 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { null | null | null | true } + def "PBS should fetch data from provider when use-dynamic-data enabled and useFetchDataRate at max value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MAX + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + imp[0].bidFloor == floorValue + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] + imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue + imp[0].ext?.prebid?.floors?.floorValue == floorValue + + ext?.prebid?.floors?.location == FETCH + ext?.prebid?.floors?.fetchStatus == SUCCESS + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + + ext?.prebid?.floors?.skipRate == floorsResponse.skipRate + ext?.prebid?.floors?.data == floorsResponse + } + } + + def "PBS shouldn't fetch data from provider when use-dynamic-data disabled and useFetchDataRate at max value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with disabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = false + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MAX + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request shouldn't contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + !ext?.prebid?.floors?.floorProvider + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == SUCCESS + } + } + + def "PBS shouldn't fetch data from provider when use-dynamic-data enabled and useFetchDataRate at min value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MIN + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request shouldn't contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + !ext?.prebid?.floors?.floorProvider + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == SUCCESS + } + } + + def "PBS should log error and increase metrics when useFetchDataRate have invalid value"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch and fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = accounntUseFetchDataRate + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "metric should be updated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[FETCH_FAILURE_METRIC] == 1 + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId") + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("reason : Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.floorProvider + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == ERROR + } + + where: + accounntUseFetchDataRate << [PBSUtils.getRandomNegativeNumber(-USE_FETCH_DATA_RATE_MAX, USE_FETCH_DATA_RATE_MIN), + PBSUtils.getRandomNumber(USE_FETCH_DATA_RATE_MAX + 1) + ] + } + def "PBS should process floors from request when use-dynamic-data = false"() { given: "Pbs with PF configuration with useDynamicData" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { @@ -394,7 +602,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with fetch.enabled, fetch.url, useDynamicData in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.useDynamicData = accountUseDynamicData } accountDao.save(account) @@ -406,7 +614,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -430,7 +638,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -451,10 +659,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to request for " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to request for " + "account $accountId, provider respond with status 400") and: "Floors validation failure cannot reject the entire auction" @@ -472,7 +680,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -491,10 +699,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " + "response for account $accountId, cause: DecodeException: Failed to decode") and: "Floors validation failure cannot reject the entire auction" @@ -512,7 +720,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -530,10 +738,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " + "response for account $accountId, response body can not be empty" as String) and: "Floors validation failure cannot reject the entire auction" @@ -551,7 +759,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -572,10 +780,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules should contain " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules should contain " + "at least one model group " as String) and: "Floors validation failure cannot reject the entire auction" @@ -593,7 +801,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -614,10 +822,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules values can't " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules values can't " + "be null or empty, but were null" as String) and: "Floors validation failure cannot reject the entire auction" @@ -635,7 +843,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def maxRules = 1 def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.fetch = fetchConfig(accountId, maxRules) @@ -659,18 +867,18 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules number " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules number " + "2 exceeded its maximum number $maxRules") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRules: max) }, - { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRulesSnakeCase: max) }] + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRules: max) }, + { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRulesSnakeCase: max) }] } def "PBS should log error and increase #FETCH_FAILURE_METRIC when fetch request exceeds fetch.timeout-ms"() { @@ -687,7 +895,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -705,9 +913,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = pbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$basicFetchUrl$accountId', " + + assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$BASIC_FETCH_URL$accountId', " + "account $accountId exceeded") and: "Floors validation failure cannot reject the entire auction" @@ -725,7 +933,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with maxFileSizeKb in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def maxSize = PBSUtils.getRandomNumber(1, 5) def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.fetch = fetchConfig(accountId, maxSize) @@ -748,35 +956,36 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Response size " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Response size " + "$responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKb: maxKbSize) }, - { String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }] + fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKb: maxKbSize) }, + { String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }] } def "PBS should prefer data from stored request when request doesn't contain floors data"() { given: "Default BidRequest with storedRequest" + def storedRequestId = PBSUtils.randomNumber as String def bidRequest = request.tap { - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) } and: "Default stored request with floors" def storedRequestModel = bidRequestWithFloors and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel) + def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel) storedRequestDao.save(storedRequest) and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(accountId).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -805,9 +1014,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } where: - request | accountId | bidRequestWithFloors - BidRequest.defaultBidRequest | request.site.publisher.id | bidRequestWithFloors - BidRequest.getDefaultBidRequest(APP) | request.app.publisher.id | getBidRequestWithFloors(APP) + request | bidRequestWithFloors + BidRequest.defaultBidRequest | getBidRequestWithFloors(SITE) + BidRequest.getDefaultBidRequest(APP) | getBidRequestWithFloors(APP) } def "PBS should prefer data from request when fetch is disabled in account config"() { @@ -815,7 +1024,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -851,7 +1060,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest - and: "Default stored request with floors " + and: "Default stored request with floors" def ampStoredRequest = storedRequestWithFloors def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -888,8 +1097,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def "PBS should prefer data from floors provider when floors data is defined in both request and stored request"() { given: "BidRequest with storedRequest" + def storedRequestId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithFloors.tap { - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) ext.prebid.floors.floorMin = FLOOR_MIN } @@ -897,11 +1107,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def storedRequestModel = bidRequestWithFloors and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel) + def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel) storedRequestDao.save(storedRequest) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -909,13 +1119,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS cache rules and processes auction request" cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) then: "Bidder request should contain floors data from floors provider" - def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last verifyAll(bidderRequest) { imp[0].bidFloor == floorValue imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency @@ -988,20 +1198,20 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider #description response" - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) then: "PBS should cache data from data provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS should periodically fetch data from data provider" - PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.app.publisher.id) > 1 }, 7000, 3000) + PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.accountId) > 1 }, 7000, 3000) where: description | floorsResponse @@ -1021,7 +1231,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider #description response" @@ -1029,7 +1239,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) @@ -1067,14 +1277,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def "PBS should validate rules from request when floorMin from request is invalid"() { given: "Default BidRequest with floorMin" def floorValue = PBSUtils.randomFloorValue - def invalidFloorMin = MIN_FLOOR_MIN - 1 + def invalidFloorMin = FLOOR_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue ext.prebid.floors.floorMin = invalidFloorMin } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1102,7 +1312,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1130,7 +1340,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1162,7 +1372,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1238,7 +1448,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1260,7 +1470,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be in range(0-100), but was $invalidSkipRate "] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when data skipRate from request is invalid"() { @@ -1278,7 +1488,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1300,7 +1510,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be in range(0-100), but was $invalidSkipRate "] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when modelGroup skipRate from request is invalid"() { @@ -1318,7 +1528,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1340,24 +1550,24 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be in range(0-100), but was $invalidSkipRate "] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should validate rules from request when default floor value from request is invalid"() { given: "Default BidRequest with default floor value" def floorValue = PBSUtils.randomFloorValue - def invalidDefaultFloorValue = MIN_DEFAULT_FLOOR_VALUE - 1 + def invalidDefaultFloorValue = DEFAULT_FLOOR_VALUE_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] - ext.prebid.floors.data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + ext.prebid.floors.data.modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1385,7 +1595,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1437,7 +1647,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1471,10 +1681,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" + " must be in range(1-100), but was $invalidModelWeight") and: "Floors validation failure cannot reject the entire auction" @@ -1495,7 +1705,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1530,17 +1740,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor data skipRate" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor data skipRate" + " must be in range(0-100), but was $invalidSkipRate") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when modelGroup skipRate from floors provider is invalid"() { @@ -1554,7 +1764,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1589,17 +1799,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + " must be in range(0-100), but was $invalidSkipRate") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when default floor value from floors provider is invalid"() { @@ -1613,19 +1823,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1 + def invalidDefaultFloor = DEFAULT_FLOOR_VALUE_MIN - 1 def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups << ModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].defaultFloor = invalidDefaultFloor modelGroups.last().values = [(rule): floorValue] - modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN } floorsProvider.setResponse(accountId, floorsResponse) @@ -1648,10 +1858,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup default" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup default" + " must be positive float, but was $invalidDefaultFloor") and: "Floors validation failure cannot reject the entire auction" @@ -1663,7 +1873,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -1673,7 +1883,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { modelGroups[0].currency = modelGroupCurrency currency = dataCurrency } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) @@ -1699,7 +1909,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1719,7 +1929,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "PBS log should not contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 0 and: "Bidder request should contain floors data from floors provider" @@ -1733,7 +1943,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1769,7 +1979,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1807,7 +2017,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index 3121923d858..478248d3f46 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -55,7 +55,7 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Prebid.Channel -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { @@ -954,7 +954,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_PRICE_FLOOR + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() where: diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy index 87c7689b4ab..e0c3b279200 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy @@ -9,6 +9,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ActivityInfrastructure import org.prebid.server.functional.model.response.auction.ActivityInvocationPayload import org.prebid.server.functional.model.response.auction.And @@ -120,7 +121,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - def bidResponse = pbsServiceFactory.getService(PBS_CONFIG).sendAuctionRequest(bidRequest) + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain basic info in debug" def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure @@ -192,7 +193,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE device = new Device(geo: new Geo(country: USA, region: ALABAMA.abbreviation)) - regs.ext.gpc = PBSUtils.randomString + regs.ext = new RegsExt(gpc: PBSUtils.randomString) regs.gppSid = [US_CA_V1.intValue] setAccountId(accountId) } @@ -298,7 +299,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE device = new Device(geo: new Geo(country: USA, region: ALABAMA.abbreviation)) - regs.ext.gpc = PBSUtils.randomString + regs.ext = new RegsExt(gpc: PBSUtils.randomString) regs.gppSid = [US_CA_V1.intValue] regs.gpp = new UsNatV1Consent.Builder().setGpc(true).build() setAccountId(accountId) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy index c1f002a9cb7..2575789049d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy @@ -6,6 +6,7 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Dsa import org.prebid.server.functional.model.request.auction.Dsa as RequestDsa +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.DsaResponse @@ -19,7 +20,7 @@ import static org.prebid.server.functional.model.request.auction.DsaRequired.NOT import static org.prebid.server.functional.model.request.auction.DsaRequired.REQUIRED import static org.prebid.server.functional.model.request.auction.DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM import static org.prebid.server.functional.model.request.auction.DsaRequired.SUPPORTED -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_DUE_TO_DSA +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_DSA import static org.prebid.server.functional.model.response.auction.DsaAdRender.ADVERTISER_WILL_RENDER import static org.prebid.server.functional.model.response.auction.DsaAdRender.ADVERTISER_WONT_RENDER import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC @@ -34,7 +35,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -63,7 +64,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -106,7 +107,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -143,7 +144,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored bid request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -177,7 +178,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should always forward DSA to bidders"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) } when: "PBS processes auction request" @@ -198,7 +199,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should always accept bids with DSA"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) } and: "Default bidder response with DSA" @@ -235,7 +236,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should accept bids without DSA when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response with DSA" @@ -263,7 +264,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should reject bids without DSA when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response without DSA" @@ -293,7 +294,7 @@ class DsaSpec extends PrivacyBaseSpec { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response without DSA" @@ -316,7 +317,7 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_DSA + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id @@ -332,7 +333,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account with default DSA config" @@ -352,7 +353,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = requestDsa + regs.ext = new RegsExt(dsa: requestDsa) } and: "Account with default DSA config" @@ -378,7 +379,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account without default DSA config" @@ -397,8 +398,8 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null - regs.ext.gdpr = 0 + regs.ext = new RegsExt(dsa: null) + regs.gdpr = 0 } and: "Account with default DSA config" @@ -424,7 +425,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = getGdprBidRequest(consentString).tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account with default DSA config" @@ -448,8 +449,8 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null - regs.ext.gdpr = 0 + regs.ext = new RegsExt(dsa: null) + regs.gdpr = 0 } and: "Account with default DSA config" @@ -471,9 +472,9 @@ class DsaSpec extends PrivacyBaseSpec { given: "Default bid request with DSA pubRender" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(REQUIRED).tap { + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(REQUIRED).tap { it.pubRender = pubRender - } + }) } and: "Default bidder response with incorrect DSA adRender" @@ -496,7 +497,7 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_DSA + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id @@ -513,7 +514,7 @@ class DsaSpec extends PrivacyBaseSpec { given: "Default bid request with DSA pubRender" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(REQUIRED) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(REQUIRED)) } and: "Default bidder response with incorrect DSA" @@ -536,7 +537,7 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_DSA + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy index 470029c9e02..771fac9bd84 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy @@ -8,9 +8,11 @@ import org.prebid.server.functional.model.config.AccountPrivacyConfig import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.container.PrebidServerContainer +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.CcpaConsent @@ -26,6 +28,8 @@ import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 import static org.prebid.server.functional.model.config.PurposeEnforcement.BASIC import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS @@ -35,6 +39,7 @@ import static org.prebid.server.functional.model.request.auction.ActivityType.FE import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.BGR_IP import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID @@ -313,10 +318,8 @@ class GdprAmpSpec extends PrivacyBaseSpec { def startTime = Instant.now() and: "Create new container" - def serverContainer = new PrebidServerContainer(GDPR_VENDOR_LIST_CONFIG + - ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]) - serverContainer.start() - def privacyPbsService = new PrebidServerService(serverContainer) + def config = GDPR_VENDOR_LIST_CONFIG + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String] + def defaultPrivacyPbsService = pbsServiceFactory.getService(config) and: "Prepare tcf consent string" def tcfConsent = new TcfConsent.Builder() @@ -340,31 +343,32 @@ class GdprAmpSpec extends PrivacyBaseSpec { vendorListResponse.setResponse(tcfPolicyVersion) when: "PBS processes amp request" - privacyPbsService.sendAmpRequest(ampRequest) + defaultPrivacyPbsService.sendAmpRequest(ampRequest) then: "Used vendor list have proper specification version of GVL" def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) - PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } - def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion and: "Logs should contain proper vendor list version" - def logs = privacyPbsService.getLogsByTime(startTime) + def logs = defaultPrivacyPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Created new TCF 2 vendor list for version " + "v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion}") - cleanup: "Stop container with default request" - serverContainer.stop() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) where: tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } - def "PBS amp with invalid consent.tcfPolicyVersion parameter should reject request and include proper warning"() { - given: "Tcf consent string" - def invalidTcfPolicyVersion = PBSUtils.getRandomNumber(5, 63) + def "PBS amp shouldn't reject request with proper warning and metrics when incoming consent.tcfPolicyVersion have invalid parameter"() { + given: "Tcf consent string with invalid tcf policy version" def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) .setTcfPolicyVersion(invalidTcfPolicyVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() and: "AMP request" @@ -377,17 +381,35 @@ class GdprAmpSpec extends PrivacyBaseSpec { def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) + and: "Flush metrics" + flushMetrics(privacyPbsService) + when: "PBS processes amp request" def response = privacyPbsService.sendAmpRequest(ampRequest) then: "Bid response should contain warning" assert response.ext?.warnings[PREBID]*.code == [999] assert response.ext?.warnings[PREBID]*.message == - ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] + ["Unknown tcfPolicyVersion ${invalidTcfPolicyVersion}, defaulting to gvlSpecificationVersion=3" as String] + + and: "Alerts.general metrics should be populated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["alerts.general"] == 1 + + and: "Bidder should be called" + assert bidder.getBidderRequest(ampStoredRequest.id) + + where: + invalidTcfPolicyVersion << [MIN_INVALID_TCF_POLICY_VERSION, + PBSUtils.getRandomNumber(MIN_INVALID_TCF_POLICY_VERSION, MAX_INVALID_TCF_POLICY_VERSION), + MAX_INVALID_TCF_POLICY_VERSION] } def "PBS amp should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { - given: "Test start time" + given: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "Test start time" def startTime = Instant.now() and: "Prepare tcf consent string" @@ -415,14 +437,14 @@ class GdprAmpSpec extends PrivacyBaseSpec { vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3)) when: "PBS processes amp request" - privacyPbsService.sendAmpRequest(ampRequest) + defaultPrivacyPbsService.sendAmpRequest(ampRequest) then: "PBS shouldn't fetch vendor list" def vendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) - assert !privacyPbsService.isFileExist(vendorListPath) + assert !defaultPrivacyPbsService.isFileExist(vendorListPath) and: "Logs should contain proper vendor list version" - def logs = privacyPbsService.getLogsByTime(startTime) + def logs = defaultPrivacyPbsService.getLogsByTime(startTime) def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading." assert getLogsByText(logs, tcfError) @@ -430,18 +452,21 @@ class GdprAmpSpec extends PrivacyBaseSpec { def secondStartTime = Instant.now() when: "PBS processes amp request" - privacyPbsService.sendAmpRequest(ampRequest) + defaultPrivacyPbsService.sendAmpRequest(ampRequest) then: "PBS shouldn't fetch vendor list" - assert !privacyPbsService.isFileExist(vendorListPath) + assert !defaultPrivacyPbsService.isFileExist(vendorListPath) and: "Logs should contain proper vendor list version" - def logsSecond = privacyPbsService.getLogsByTime(secondStartTime) + def logsSecond = defaultPrivacyPbsService.getLogsByTime(secondStartTime) assert getLogsByText(logsSecond, tcfError) and: "Reset vendor list response" vendorListResponse.reset() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + where: tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } @@ -634,4 +659,203 @@ class GdprAmpSpec extends PrivacyBaseSpec { assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] } + + def "PBS amp should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() { + given: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "AMP request" + def ampRequest = getGdprAmpRequest(tcfConsent) + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes amp request" + defaultPrivacyPbsService.sendAmpRequest(ampRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] + } + + def "PBS should process with GDPR enforcement when GDPR and COPPA configurations are present in request"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Amp default request" + def ampRequest = getGdprAmpRequest(validConsentString) + + and: "Bid request with gdpr and coppa config" + def ampStoredRequest = getGdprBidRequest(DistributionChannel.SITE, validConsentString).tap { + regs = new Regs(gdpr: gdpr, coppa: coppa, ext: new RegsExt(gdpr: extGdpr, coppa: extCoppa)) + setAccountId(ampRequest.account) + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: PBSUtils.getRandomEnum(Country.class, [BULGARIA])) + def account = getAccountWithGdpr(ampRequest.account, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Bidder shouldn't be called" + assert !bidder.getBidderRequests(ampStoredRequest.id) + + then: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + + where: + gdpr | coppa | extGdpr | extCoppa + 1 | 1 | 1 | 1 + 1 | 1 | 1 | 0 + 1 | 1 | 1 | null + 1 | 1 | 0 | 1 + 1 | 1 | 0 | 0 + 1 | 1 | 0 | null + 1 | 1 | null | 1 + 1 | 1 | null | 0 + 1 | 1 | null | null + 1 | 0 | 1 | 1 + 1 | 0 | 1 | 0 + 1 | 0 | 1 | null + 1 | 0 | 0 | 1 + 1 | 0 | 0 | 0 + 1 | 0 | 0 | null + 1 | 0 | null | 1 + 1 | 0 | null | 0 + 1 | 0 | null | null + 1 | null | 1 | 1 + 1 | null | 1 | 0 + 1 | null | 1 | null + 1 | null | 0 | 1 + 1 | null | 0 | 0 + 1 | null | 0 | null + 1 | null | null | 1 + 1 | null | null | 0 + 1 | null | null | null + + null | 1 | 1 | 1 + null | 1 | 1 | 0 + null | 1 | 1 | null + null | 0 | 1 | 1 + null | 0 | 1 | 0 + null | 0 | 1 | null + null | null | 1 | 1 + null | null | 1 | 0 + null | null | 1 | null + } + + def "PBS should process with GDPR enforcement when request comes from EEA IP with COPPA enabled"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Amp default request" + def ampRequest = getGdprAmpRequest(validConsentString) + + and: "Bid request with gdpr and coppa config" + def ampStoredRequest = getGdprBidRequest(DistributionChannel.SITE, validConsentString).tap { + regs = new Regs(gdpr: 1, coppa: 1, ext: new RegsExt(gdpr: 1, coppa: 1)) + device.geo.country = requestCountry + device.geo.region = null + device.ip = requestIpV4 + device.ipv6 = requestIpV6 + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: accountCountry) + def account = getAccountWithGdpr(ampRequest.account, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest, header) + + then: "Bidder shouldn't be called" + assert !bidder.getBidderRequests(ampStoredRequest.id) + + then: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + + where: + requestCountry | accountCountry | requestIpV4 | requestIpV6 | header + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | BGR_IP.v4 | null | [:] + BULGARIA | null | BGR_IP.v4 | null | [:] + BULGARIA | BULGARIA | null | BGR_IP.v6 | [:] + BULGARIA | null | null | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | null | null | [:] + BULGARIA | null | null | null | [:] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + null | null | BGR_IP.v4 | BGR_IP.v6 | [:] + null | BULGARIA | BGR_IP.v4 | null | [:] + null | null | BGR_IP.v4 | null | [:] + null | BULGARIA | null | BGR_IP.v6 | [:] + null | null | null | BGR_IP.v6 | [:] + null | BULGARIA | null | null | [:] + null | null | null | null | [:] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 89a6899e8ac..351875e5f90 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -5,11 +5,11 @@ import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.config.PurposeEnforcement +import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Regs import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ErrorType -import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.TcfConsent @@ -21,13 +21,14 @@ import java.time.Instant import static org.prebid.server.functional.model.ChannelType.PBJS import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA -import static org.prebid.server.functional.model.pricefloors.Country.CAN -import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.config.Purpose.P1 import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 -import static org.prebid.server.functional.model.config.PurposeEnforcement.* +import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA +import static org.prebid.server.functional.model.pricefloors.Country.CAN +import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT @@ -36,9 +37,10 @@ import static org.prebid.server.functional.model.request.auction.ActivityType.TR import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.Prebid.Channel +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.BGR_IP import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_PRIVACY +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_PRIVACY import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS @@ -263,7 +265,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_BY_PRIVACY + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_PRIVACY and: "seatbid should be empty" assert response.seatbid.isEmpty() @@ -274,10 +276,8 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def startTime = Instant.now() and: "Create new container" - def serverContainer = new PrebidServerContainer(GDPR_VENDOR_LIST_CONFIG + - ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]) - serverContainer.start() - def privacyPbsService = new PrebidServerService(serverContainer) + def config = GDPR_VENDOR_LIST_CONFIG + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String] + def defaultPrivacyPbsService = pbsServiceFactory.getService(config) and: "Tcf consent setup" def tcfConsent = new TcfConsent.Builder() @@ -294,31 +294,32 @@ class GdprAuctionSpec extends PrivacyBaseSpec { vendorListResponse.setResponse(tcfPolicyVersion) when: "PBS processes auction request" - privacyPbsService.sendAuctionRequest(bidRequest) + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) then: "Used vendor list have proper specification version of GVL" def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) - PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } - def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion and: "Logs should contain proper vendor list version" - def logs = privacyPbsService.getLogsByTime(startTime) + def logs = defaultPrivacyPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Created new TCF 2 vendor list for version " + "v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion}") - cleanup: "Stop container with default request" - serverContainer.stop() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) where: tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } - def "PBS auction should reject request with proper warning when incoming consent.tcfPolicyVersion have invalid parameter"() { - given: "Tcf consent string" - def invalidTcfPolicyVersion = PBSUtils.getRandomNumber(5, 63) + def "PBS auction shouldn't reject request with proper warning and metrics when incoming consent.tcfPolicyVersion have invalid parameter"() { + given: "Tcf consent string with invalid tcf policy version" def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) .setTcfPolicyVersion(invalidTcfPolicyVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() and: "Bid request" @@ -333,11 +334,29 @@ class GdprAuctionSpec extends PrivacyBaseSpec { then: "Bid response should contain warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] assert response.ext?.warnings[ErrorType.PREBID]*.message == - ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] + ["Unknown tcfPolicyVersion ${invalidTcfPolicyVersion}, defaulting to gvlSpecificationVersion=3" as String] + + and: "Alerts.general metrics should be populated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["alerts.general"] == 1 + + and: "Bid response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Bidder should be called" + assert bidder.getBidderRequest(bidRequest.id) + + where: + invalidTcfPolicyVersion << [MIN_INVALID_TCF_POLICY_VERSION, + PBSUtils.getRandomNumber(MIN_INVALID_TCF_POLICY_VERSION, MAX_INVALID_TCF_POLICY_VERSION), + MAX_INVALID_TCF_POLICY_VERSION] } def "PBS auction should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { - given: "Test start time" + given: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "Test start time" def startTime = Instant.now() and: "Tcf consent setup" @@ -358,14 +377,14 @@ class GdprAuctionSpec extends PrivacyBaseSpec { vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3)) when: "PBS processes auction request" - privacyPbsService.sendAuctionRequest(bidRequest) + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) then: "Used vendor list have proper specification version of GVL" def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) - assert !privacyPbsService.isFileExist(properVendorListPath) + assert !defaultPrivacyPbsService.isFileExist(properVendorListPath) and: "Logs should contain proper vendor list version" - def logs = privacyPbsService.getLogsByTime(startTime) + def logs = defaultPrivacyPbsService.getLogsByTime(startTime) def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading." assert getLogsByText(logs, tcfError) @@ -373,18 +392,21 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def secondStartTime = Instant.now() when: "PBS processes amp request" - privacyPbsService.sendAuctionRequest(bidRequest) + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) then: "PBS shouldn't fetch vendor list" - assert !privacyPbsService.isFileExist(properVendorListPath) + assert !defaultPrivacyPbsService.isFileExist(properVendorListPath) and: "Logs should contain proper vendor list version" - def logsSecond = privacyPbsService.getLogsByTime(secondStartTime) + def logsSecond = defaultPrivacyPbsService.getLogsByTime(secondStartTime) assert getLogsByText(logsSecond, tcfError) and: "Reset vendor list response" vendorListResponse.reset() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + where: tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } @@ -505,7 +527,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic bid requests with personal data" def tcfConsent = new TcfConsent.Builder().build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent } @@ -535,7 +557,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic BidRequests with personal data" def tcfConsent = new TcfConsent.Builder().build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent ext.prebid.trace = VERBOSE } @@ -613,7 +635,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic BidRequests with personal data" def tcfConsent = new TcfConsent.Builder().build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent ext.prebid.trace = BASIC } @@ -693,7 +715,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic BidRequests with privacy data" def tcfConsent = new TcfConsent.Builder().setSpecialFeatureOptIns(DEVICE_ACCESS).build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent } @@ -761,4 +783,181 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] } + + def "PBS auction should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() { + given: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes auction request" + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] + } + + def "PBS should process with GDPR enforcement when GDPR and COPPA configurations are present in request"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request with gdpr and coppa config" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + regs = new Regs(gdpr: gdpr, coppa: coppa, ext: new RegsExt(gdpr: extGdpr, coppa: extCoppa)) + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: PBSUtils.getRandomEnum(Country.class, [BULGARIA])) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder shouldn't be called" + assert !bidder.getBidderRequests(bidRequest.id) + + then: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: + gdpr | coppa | extGdpr | extCoppa + 1 | 1 | 1 | 1 + 1 | 1 | 1 | 0 + 1 | 1 | 1 | null + 1 | 1 | 0 | 1 + 1 | 1 | 0 | 0 + 1 | 1 | 0 | null + 1 | 1 | null | 1 + 1 | 1 | null | 0 + 1 | 1 | null | null + 1 | 0 | 1 | 1 + 1 | 0 | 1 | 0 + 1 | 0 | 1 | null + 1 | 0 | 0 | 1 + 1 | 0 | 0 | 0 + 1 | 0 | 0 | null + 1 | 0 | null | 1 + 1 | 0 | null | 0 + 1 | 0 | null | null + 1 | null | 1 | 1 + 1 | null | 1 | 0 + 1 | null | 1 | null + 1 | null | 0 | 1 + 1 | null | 0 | 0 + 1 | null | 0 | null + 1 | null | null | 1 + 1 | null | null | 0 + 1 | null | null | null + + null | 1 | 1 | 1 + null | 1 | 1 | 0 + null | 1 | 1 | null + null | 0 | 1 | 1 + null | 0 | 1 | 0 + null | 0 | 1 | null + null | null | 1 | 1 + null | null | 1 | 0 + null | null | 1 | null + } + + def "PBS should process with GDPR enforcement when request comes from EEA IP with COPPA enabled"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request with gdpr and coppa config" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + regs = new Regs(gdpr: 1, coppa: 1, ext: new RegsExt(gdpr: 1, coppa: 1)) + device.geo.country = requestCountry + device.geo.region = null + device.ip = requestIpV4 + device.ipv6 = requestIpV6 + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: accountCountry) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest, header) + + then: "Bidder shouldn't be called" + assert !bidder.getBidderRequests(bidRequest.id) + + then: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: + requestCountry | accountCountry | requestIpV4 | requestIpV6 | header + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | BGR_IP.v4 | null | [:] + BULGARIA | null | BGR_IP.v4 | null | [:] + BULGARIA | BULGARIA | null | BGR_IP.v6 | [:] + BULGARIA | null | null | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | null | null | [:] + BULGARIA | null | null | null | [:] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + null | null | BGR_IP.v4 | BGR_IP.v6 | [:] + null | BULGARIA | BGR_IP.v4 | null | [:] + null | null | BGR_IP.v4 | null | [:] + null | BULGARIA | null | BGR_IP.v6 | [:] + null | null | null | BGR_IP.v6 | [:] + null | BULGARIA | null | null | [:] + null | null | null | null | [:] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy index db59d60a260..204f2eb7025 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy @@ -5,6 +5,7 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.amp.ConsentType import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent import org.prebid.server.functional.util.privacy.gpp.UsV1Consent @@ -188,7 +189,7 @@ class GppAmpSpec extends PrivacyBaseSpec { and: "Save storedRequest into DB" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -212,7 +213,7 @@ class GppAmpSpec extends PrivacyBaseSpec { and: "Save storedRequest into DB" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -222,6 +223,6 @@ class GppAmpSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain gpc value from header" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - assert !bidderRequest.regs.ext + assert !bidderRequest?.regs?.ext?.gpc } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy index cef812e4cf5..4eb78ee2140 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils @@ -235,7 +236,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS should populate gpc when header sec-gpc has value 1"() { given: "Default bid request with gpc" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } when: "PBS processes auction request with headers" @@ -252,7 +253,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS shouldn't populate gpc when header sec-gpc has #gpcInvalid value"() { given: "Default bid request with gpc" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } when: "PBS processes auction request with headers" @@ -260,7 +261,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain gpc from header" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - assert !bidderRequests.regs.ext + assert !bidderRequests?.regs?.ext?.gpc where: gpcInvalid << [PBSUtils.randomNumber as String, PBSUtils.randomNumber, PBSUtils.randomString, Boolean.TRUE] @@ -270,7 +271,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { given: "Default bid request with gpc" def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = randomGpc + regs.ext = new RegsExt(gpc: randomGpc) } when: "PBS processes auction request with headers" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy index d92dbb89d7c..db74b18ee48 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy @@ -16,6 +16,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -445,7 +446,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = randomGpc + it.regs.ext = new RegsExt(gpc: randomGpc) } and: "Setup activity" @@ -483,7 +484,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup activity" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy index 602fbb87347..f684cb10313 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy @@ -1778,7 +1778,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS cookie sync should process rule when geo doesn't intersection"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.geo-info.[0].country": countyConfig, "geolocation.configurations.geo-info.[0].region" : regionConfig]) @@ -1830,7 +1830,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS setuid should process rule when geo doesn't intersection"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": countyConfig, "geolocation.configurations.[0].geo-info.region" : regionConfig]) @@ -1885,7 +1885,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS cookie sync should disallowed rule when device.geo intersection"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": countyConfig, "geolocation.configurations.[0].geo-info.region" : regionConfig]) @@ -1936,7 +1936,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS setuid should disallowed rule when device.geo intersection"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": countyConfig, "geolocation.configurations.[0].geo-info.region" : regionConfig]) @@ -1986,7 +1986,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS cookie sync should fetch geo once when gpp sync user and account require geo look up"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": USA.ISOAlpha3, "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation]) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index 5bda3ca3758..42ee2290663 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -14,6 +14,7 @@ import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -447,7 +448,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -487,7 +488,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def gpc = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = gpc + it.regs.ext = new RegsExt(gpc: gpc) } and: "Setup activity" @@ -530,7 +531,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -570,7 +571,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup activity" @@ -1487,7 +1488,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { - regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "amp request with link to account" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy index 8f785046e45..e51a9b8f193 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy @@ -13,6 +13,7 @@ import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -731,7 +732,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -793,7 +794,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.setAccountId(accountId) it.regs.gppSid = null it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = "1" + it.regs.ext = new RegsExt(gpc: "1") } and: "Setup activity" @@ -861,7 +862,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -922,7 +923,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup condition" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index 44017052970..bbb019d515d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -21,6 +21,7 @@ import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.request.auction.UserExtData @@ -607,7 +608,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -666,7 +667,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def gpc = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = gpc + it.regs.ext = new RegsExt(gpc: gpc) } and: "Setup activity" @@ -724,7 +725,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -783,7 +784,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup activity" @@ -1965,7 +1966,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { - regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "amp request with link to account" @@ -3088,6 +3089,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { it.user.gender = PBSUtils.randomString it.user.geo = Geo.FPDGeo it.user.ext = new UserExt(data: new UserExtData(buyeruid: PBSUtils.randomString)) + it.regs.ext ?= new RegsExt() } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index a5b2e12d4a5..d4cbf17e2dd 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -23,7 +23,6 @@ import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo import org.prebid.server.functional.model.request.auction.GeoExt import org.prebid.server.functional.model.request.auction.GeoExtGeoProvider -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.request.auction.UserExtData @@ -64,12 +63,14 @@ abstract class PrivacyBaseSpec extends BaseSpec { private static final int GEO_PRECISION = 2 - protected static final Map GENERIC_COOKIE_SYNC_CONFIG = ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : "$networkServiceContainer.rootUri/generic-usersync".toString(), - "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": false.toString()] - private static final Map OPENX_COOKIE_SYNC_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", - "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value] - private static final Map OPENX_CONFIG = ["adapters.${OPENX.value}.endpoint": "$networkServiceContainer.rootUri/auction".toString(), - "adapters.${OPENX.value}.enabled" : 'true'] + protected static final Map GENERIC_CONFIG = ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : "$networkServiceContainer.rootUri/generic-usersync".toString(), + "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": false.toString(), + "adapters.${GENERIC.value}.ortb-version" : "2.6"] + private static final Map OPENX_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", + "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value, + "adapters.${OPENX}.ortb-version" : "2.6", + "adapters.${OPENX.value}.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.${OPENX.value}.enabled" : 'true'] protected static final Map GDPR_VENDOR_LIST_CONFIG = ["gdpr.vendorlist.v2.http-endpoint-template": "$networkServiceContainer.rootUri/v2/vendor-list.json".toString(), "gdpr.vendorlist.v3.http-endpoint-template": "$networkServiceContainer.rootUri/v3/vendor-list.json".toString()] protected static final Map SETTING_CONFIG = ["settings.enforce-valid-account": 'true'] @@ -102,16 +103,17 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) + protected static final Integer MAX_INVALID_TCF_POLICY_VERSION = 63 + protected static final Integer MIN_INVALID_TCF_POLICY_VERSION = 6 - @Shared - protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG + GDPR_EEA_COUNTRY) + protected static final Map GENERAL_PRIVACY_CONFIG = + GENERIC_CONFIG + GDPR_VENDOR_LIST_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG - protected static final Map PBS_CONFIG = OPENX_CONFIG + OPENX_COOKIE_SYNC_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG + @Shared + protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GDPR_EEA_COUNTRY) @Shared - protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(PBS_CONFIG) + protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(OPENX_CONFIG + SETTING_CONFIG + GENERAL_PRIVACY_CONFIG) def setupSpec() { vendorListResponse.setResponse() @@ -149,7 +151,7 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static BidRequest getBidRequestWithPersonalData(String accountId = null, DistributionChannel channel = SITE) { getBidRequestWithGeo(channel).tap { - if(accountId != null) { + if (accountId != null) { setAccountId(accountId) } ext.prebid.trace = VERBOSE @@ -183,14 +185,14 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static BidRequest getCcpaBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { getBidRequestWithGeo(channel).tap { - regs.ext = new RegsExt(usPrivacy: consentString) + regs.usPrivacy = consentString } } protected static BidRequest getGdprBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { getBidRequestWithGeo(channel).tap { - regs.ext = new RegsExt(gdpr: 1) - user = new User(ext: new UserExt(consent: consentString)) + regs.gdpr = 1 + user = new User(consent: consentString) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy index 5ca9e9d062b..882595060fa 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy @@ -27,8 +27,8 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB class TcfBasicTransmitEidsActivitiesSpec extends PrivacyBaseSpec { - private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_COOKIE_SYNC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, - "gdpr.vendorlist.v3.http-endpoint-template": null] + private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, + "gdpr.vendorlist.v3.http-endpoint-template": null] private final PrebidServerService activityPbsServiceExcludeGvl = pbsServiceFactory.getService(PBS_CONFIG) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy index 934aa69de33..d798bc4c478 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy @@ -25,7 +25,7 @@ class TcfFullTransmitEidsActivitiesSpec extends PrivacyBaseSpec { private static PrebidServerService privacyPbsServiceWithMultipleGvl def setupSpec() { - privacyPbsContainerWithMultipleGvl = new PrebidServerContainer(PBS_CONFIG) + privacyPbsContainerWithMultipleGvl = new PrebidServerContainer(GENERAL_PRIVACY_CONFIG) def prepareEncodeResponseBodyWithPurposesOnly = getVendorListContent(true, false, false) def prepareEncodeResponseBodyWithLegIntPurposes = getVendorListContent(false, true, false) def prepareEncodeResponseBodyWithLegIntAndFlexiblePurposes = getVendorListContent(false, true, true) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy index 8ca804717ed..d0fa0583685 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy @@ -28,8 +28,8 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB class TransmitEidsOrtbConverterActivitiesSpec extends PrivacyBaseSpec { - private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_COOKIE_SYNC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, - "gdpr.vendorlist.v3.http-endpoint-template": null] + private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, + "gdpr.vendorlist.v3.http-endpoint-template": null] private final PrebidServerService activityPbsServiceExcludeGvlWithElderOrtb = pbsServiceFactory.getService(PBS_CONFIG + ["adapters.generic.ortb-version": "2.5"]) @Shared diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy new file mode 100644 index 00000000000..3a87be7b9e7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy @@ -0,0 +1,118 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.AccountStatus +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.util.PBSUtils + +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED + +class AccountS3Spec extends StorageBaseSpec { + + protected PrebidServerService s3StorageAccountPbsService = PbsServiceFactory.getService(s3StorageConfig + + mySqlDisabledConfig + + ['settings.enforce-valid-account': 'true']) + + def "PBS should process request when active account is present in S3 storage"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Active account config" + def account = new AccountConfig(id: accountId, status: AccountStatus.ACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account) + + when: "PBS processes auction request" + def response = s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatbid" + assert response.seatbid.size() == 1 + } + + def "PBS should throw exception when inactive account is present in S3 storage"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Inactive account config" + def account = new AccountConfig(id: accountId, status: AccountStatus.INACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account) + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Account $accountId is inactive" + } + + def "PBS should throw exception when account id isn't match with bid request account id"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Account config with different accountId" + def account = new AccountConfig(id: PBSUtils.randomString, status: AccountStatus.ACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account, accountId) + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } + + def "PBS should throw exception when account is invalid in S3 storage json file"() { + given: "Default BidRequest" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Saved invalid account in AWS S3 storage" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_ACCOUNT_DIR}/${accountId}.json") + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } + + def "PBS should throw exception when account is not present in S3 storage and valid account enforced"() { + given: "Default BidRequest" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy new file mode 100644 index 00000000000..e6dda6b407c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy @@ -0,0 +1,115 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class AmpS3Spec extends StorageBaseSpec { + + def "PBS should take parameters from the stored request on S3 service when it's not specified in the request"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest) + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain parameters from the stored request" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + assert bidderRequest.site?.page == ampStoredRequest.site.page + assert bidderRequest.site?.publisher?.id == ampStoredRequest.site.publisher.id + assert !bidderRequest.imp[0]?.tagId + assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height + assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr + } + + @PendingFeature + def "PBS should throw exception when trying to take parameters from the stored request on S3 service with invalid id in file"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest).tap { + it.requestId = PBSUtils.randomNumber + } + s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest, ampRequest.tagId) + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored request found for id: ${ampRequest.tagId}" + } + + def "PBS should throw exception when trying to take parameters from request where id isn't match with stored request id"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_REQUEST_DIR}/${ampRequest.tagId}.json") + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "Can't parse Json for stored request with id ${ampRequest.tagId}" + } + + def "PBS should throw an exception when trying to take parameters from stored request on S3 service that do not exist"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored request found for id: ${ampRequest.tagId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy new file mode 100644 index 00000000000..51d39dd5af9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy @@ -0,0 +1,117 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.SecurityLevel +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class AuctionS3Spec extends StorageBaseSpec { + + def "PBS auction should populate imp[0].secure depend which value in imp stored request from S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp into S3 service" + def secureStoredRequest = PBSUtils.getRandomEnum(SecurityLevel.class) + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression.tap { + secure = secureStoredRequest + } + } + s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain imp[0].secure same value as in request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].secure == secureStoredRequest + } + + @PendingFeature + def "PBS should throw exception when trying to populate imp[0].secure from imp stored request on S3 service with impId that doesn't matches"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp with different impId into S3 service" + def secureStoredRequest = PBSUtils.getRandomNumber(0, 1) + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impId = PBSUtils.randomString + impData = Imp.defaultImpression.tap { + it.secure = secureStoredRequest + } + } + s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp, storedRequestId) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored impression found for id: ${storedRequestId}" + } + + def "PBS should throw exception when trying to populate imp[0].secure from invalid imp stored request on S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp into S3 service" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_IMPS_DIR}/${storedRequestId}.json" ) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "Can't parse Json for stored request with id ${storedRequestId}" + } + + def "PBS should throw exception when trying to populate imp[0].secure from unexciting imp stored request on S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored impression found for id: ${storedRequestId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy new file mode 100644 index 00000000000..583d6d97e06 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy @@ -0,0 +1,56 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.testcontainers.Dependencies +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.tests.BaseSpec +import org.prebid.server.functional.util.PBSUtils + +class StorageBaseSpec extends BaseSpec { + + protected static final String INVALID_FILE_BODY = 'INVALID' + protected static final String DEFAULT_BUCKET = PBSUtils.randomString.toLowerCase() + + protected static final S3Service s3Service = new S3Service(Dependencies.localStackContainer) + + def setupSpec() { + s3Service.createBucket(DEFAULT_BUCKET) + } + + def cleanupSpec() { + s3Service.purgeBucketFiles(DEFAULT_BUCKET) + s3Service.deleteBucket(DEFAULT_BUCKET) + } + + protected static Map s3StorageConfig = [ + 'settings.s3.accessKeyId' : s3Service.accessKeyId, + 'settings.s3.secretAccessKey' : s3Service.secretKeyId, + 'settings.s3.endpoint' : s3Service.endpoint, + 'settings.s3.bucket' : DEFAULT_BUCKET, + 'settings.s3.region' : s3Service.region, + 'settings.s3.force-path-style' : 'true', + 'settings.s3.accounts-dir' : S3Service.DEFAULT_ACCOUNT_DIR, + 'settings.s3.stored-imps-dir' : S3Service.DEFAULT_IMPS_DIR, + 'settings.s3.stored-requests-dir' : S3Service.DEFAULT_REQUEST_DIR, + 'settings.s3.stored-responses-dir': S3Service.DEFAULT_RESPONSE_DIR, + ] + + protected static Map mySqlDisabledConfig = + ['settings.database.type' : null, + 'settings.database.host' : null, + 'settings.database.port' : null, + 'settings.database.dbname' : null, + 'settings.database.user' : null, + 'settings.database.password' : null, + 'settings.database.pool-size' : null, + 'settings.database.provider-class' : null, + 'settings.database.account-query' : null, + 'settings.database.stored-requests-query' : null, + 'settings.database.amp-stored-requests-query': null, + 'settings.database.stored-responses-query' : null + ].asImmutable() as Map + + + protected PrebidServerService s3StoragePbsService = PbsServiceFactory.getService(s3StorageConfig + mySqlDisabledConfig) +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy new file mode 100644 index 00000000000..e07b5b71f2e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy @@ -0,0 +1,99 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class StoredResponseS3Spec extends StorageBaseSpec { + + def "PBS should return info from S3 stored auction response when it defined in request"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Stored auction response in S3 storage" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: storedAuctionResponse) + s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse) + + when: "PBS processes auction request" + def response = s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored auction response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedAuctionResponse.seat + assert response.seatbid[0]?.bid?.size() == storedAuctionResponse.bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedAuctionResponse.bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedAuctionResponse.bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedAuctionResponse.bid[0].id + + and: "PBS not send request to bidder" + assert !bidder.getRequestCount(bidRequest.id) + } + + @PendingFeature + def "PBS should throw request format exception when stored auction response id isn't match with requested response id"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Stored auction response in S3 storage with different id" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: PBSUtils.randomNumber, + storedAuctionResponse: storedAuctionResponse) + s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse, storedResponseId as String) + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " + + "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}." + } + + def "PBS should throw request format exception when invalid stored auction response defined in S3 storage"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Invalid stored auction response in S3 storage" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_RESPONSE_DIR}/${storedResponseId}.json") + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Can't parse Json for stored response with id ${storedResponseId}" + } + + def "PBS should throw request format exception when stored auction response defined in request but not defined in S3 storage"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " + + "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}." + } +} diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index 0d72dcfe4c6..9ac2c148b5b 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -107,9 +107,9 @@ class PBSUtils implements ObjectMapperWrapper { getRandomDecimal(min, max).setScale(scale, HALF_UP) } - static > T getRandomEnum(Class anEnum) { - def values = anEnum.enumConstants - values[getRandomNumber(0, values.length - 1)] + static > T getRandomEnum(Class anEnum, List exclude = []) { + def values = anEnum.enumConstants.findAll { !exclude.contains(it) } as T[] + values[getRandomNumber(0, values.size() - 1)] } static String convertCase(String input, Case caseType) { @@ -128,4 +128,27 @@ class PBSUtils implements ObjectMapperWrapper { throw new IllegalArgumentException("Unknown case type: $caseType") } } + + static String getRandomVersion(String minVersion = "0.0.0", String maxVersion = "99.99.99") { + def minParts = minVersion.split('\\.').collect { it.toInteger() } + def maxParts = maxVersion.split('\\.').collect { it.toInteger() } + def versionParts = [] + + def major = getRandomNumber(minParts[0], maxParts[0]) + versionParts << major + + def minorMin = (major == minParts[0]) ? minParts[1] : 0 + def minorMax = (major == maxParts[0]) ? maxParts[1] : 99 + def minor = getRandomNumber(minorMin, minorMax) + versionParts << minor + + if (minParts.size() > 2 || maxParts.size() > 2) { + def patchMin = (major == minParts[0] && minor == minParts[1]) ? minParts[2] : 0 + def patchMax = (major == maxParts[0] && minor == maxParts[1]) ? maxParts[2] : 99 + def patch = getRandomNumber(patchMin, patchMax) + versionParts << patch + } + def version = versionParts.join('.') + return (version >= minVersion && version <= maxVersion) ? version : getRandomVersion(minVersion, maxVersion) + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy b/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy index 6cdb42568ed..4ea50effbce 100644 --- a/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy @@ -1,11 +1,12 @@ package org.prebid.server.functional.util.privacy import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion @JsonIgnoreProperties(ignoreUnknown = true) class VendorListConsent { Integer vendorListVersion Integer tcfPolicyVersion - Integer gvlSpecificationVersion + GvlSpecificationVersion gvlSpecificationVersion } diff --git a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java index 35f829a4c38..4f81b761e59 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java @@ -19,6 +19,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; +import org.prebid.server.VertxTest; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -36,10 +37,14 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -58,7 +63,7 @@ import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -public class AnalyticsReporterDelegatorTest { +public class AnalyticsReporterDelegatorTest extends VertxTest { private static final String EVENT = StringUtils.EMPTY; private static final Integer FIRST_REPORTER_ID = 1; @@ -100,7 +105,14 @@ public void setUp() { .willReturn(Future.succeededFuture(enforcementActionMap)); target = new AnalyticsReporterDelegator( - vertx, List.of(firstReporter, secondReporter), tcfEnforcement, userFpdActivityMask, metrics, 0.01); + vertx, + List.of(firstReporter, secondReporter), + tcfEnforcement, + userFpdActivityMask, + metrics, + 0.01, + Set.of("logAnalytics", "adapter"), + jacksonMapper); } @Test @@ -387,6 +399,154 @@ public void shouldUpdateAuctionEventToConsideringActivitiesRestrictions() { }); } + @Test + public void shouldNotCallAnalyticsAdapterIfDisabledByAccount() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("enabled", false); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(mapper.createObjectNode()).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx).runOnContext(any()); + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(firstReporter, never()).processEvent(auctionEventCaptor.capture()); + } + + @Test + public void shouldCallAnalyticsAdapterIfAdapterNodePresentButEnabledPropertyNotPresent() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(mapper.createObjectNode()).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx, times(2)).runOnContext(any()); + verify(firstReporter).processEvent(any()); + } + + @Test + public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConfig() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("enabled", true); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(mapper.createObjectNode()).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx, times(2)).runOnContext(any()); + + final ObjectNode expectedAnalyticsNode = mapper.createObjectNode(); + final ObjectNode expectedLogAnalyticsNode = mapper.createObjectNode(); + expectedLogAnalyticsNode.put("property1", "value1"); + expectedLogAnalyticsNode.put("property2", "value2"); + expectedAnalyticsNode.set("logAnalytics", expectedLogAnalyticsNode); + + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(firstReporter).processEvent(auctionEventCaptor.capture()); + assertThat(auctionEventCaptor.getValue()) + .extracting(AuctionEvent::getAuctionContext) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getAnalytics) + .isEqualTo(expectedAnalyticsNode); + } + + @Test + public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConfigWithPrecedenceForRequest() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("enabled", true); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode logAnalyticsNode = mapper.createObjectNode(); + logAnalyticsNode.put("property1", "requestValue1"); + logAnalyticsNode.put("property3", "requestValue3"); + analyticsNode.set("logAnalytics", logAnalyticsNode); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("channel")) + .analytics(analyticsNode) + .build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx, times(2)).runOnContext(any()); + + final ObjectNode expectedAnalyticsNode = mapper.createObjectNode(); + final ObjectNode expectedLogAnalyticsNode = mapper.createObjectNode(); + expectedLogAnalyticsNode.put("property1", "requestValue1"); + expectedLogAnalyticsNode.put("property2", "value2"); + expectedLogAnalyticsNode.put("property3", "requestValue3"); + expectedAnalyticsNode.set("logAnalytics", expectedLogAnalyticsNode); + + final ExtRequestPrebid expectedExtRequestPrebid = ExtRequestPrebid.builder() + .analytics(expectedAnalyticsNode) + .channel(ExtRequestPrebidChannel.of("channel")) + .build(); + + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(firstReporter).processEvent(auctionEventCaptor.capture()); + assertThat(auctionEventCaptor.getValue()) + .extracting(AuctionEvent::getAuctionContext) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .isEqualTo(expectedExtRequestPrebid); + } + @SuppressWarnings("unchecked") private static Answer withNullAndInvokeHandler() { return invocation -> { @@ -412,6 +572,7 @@ private static AuctionEvent givenAuctionEvent( return AuctionEvent.builder() .auctionContext(AuctionContext.builder() + .account(Account.builder().build()) .bidRequest(bidRequestCustomizer.apply(BidRequest.builder()).build()) .build()) .build(); diff --git a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java new file mode 100644 index 00000000000..e7ce7126031 --- /dev/null +++ b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java @@ -0,0 +1,612 @@ +package org.prebid.server.analytics.reporter.agma; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iabtcf.decoder.TCString; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; +import org.prebid.server.analytics.reporter.agma.model.AgmaEvent; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import static io.vertx.core.http.HttpMethod.POST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class AgmaAnalyticsReporterTest extends VertxTest { + + private static final String VALID_CONSENT = + "CQEXy8AQEXy8APoABABGBFEAAACAAAAAAAAAIxQAQIxAAAAA.QIxQAQIxAAAA.IAAA"; + private static final String CONSENT_WITHOUT_PURPOSE_9 = + "CQEXy8AQEXy8APoABABGBFEAAACAAAAAAAAAIxQAQIxAAAAA.QIxQAQIxAAAA.IAAA"; + private static final String CONSENT_WITHOUT_VENDOR = + "CQEXy8AQEXy8APoABABGBFEAAACAAAAAAAAAIwwAQIwgAAAA.QJJQAQJJAAAA.IAAA"; + + private static final TCString PARSED_VALID_CONSENT = TCString.decode(VALID_CONSENT); + + @Mock(strictness = Mock.Strictness.LENIENT) + private Vertx vertx; + + @Mock(strictness = Mock.Strictness.LENIENT) + private HttpClient httpClient; + + @Mock + private PrebidVersionProvider versionProvider; + + @Captor + private ArgumentCaptor headersCaptor; + + private Clock clock; + + private AgmaAnalyticsReporter target; + + @BeforeEach + public void setUp() { + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of( + "publisherId", "accountCode", + "unknown_publisherId", "anotherCode")) + .build(); + + clock = Clock.fixed(Instant.parse("2024-09-03T10:00:00Z"), ZoneId.of("UTC+05:00")); + + given(versionProvider.getNameVersionRecord()).willReturn("pbs_version"); + given(vertx.setTimer(anyLong(), any())).willReturn(1L, 2L); + given(httpClient.request(eq(POST), anyString(), any(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), ""))); + given(httpClient.request(eq(POST), anyString(), any(), any(byte[].class), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), ""))); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + } + + @Test + public void processEventShouldNotSendAnythingWhenAuctionContextIsNull() { + // given + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(null) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendEventWhenEventIsAuctionEvent() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendEventWhenEventIsVideoEvent() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final VideoEvent videoEvent = VideoEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(videoEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("video") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendEventWhenEventIsAmpEvent() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AmpEvent ampEvent = AmpEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(ampEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("amp") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenEventIsNotAuctionAmpOrVideo() { + // given + final NotificationEvent notificationEvent = NotificationEvent.builder().build(); + + // when + final Future result = target.processEvent(notificationEvent); + + // then + assertThat(result.succeeded()).isTrue(); + verifyNoInteractions(httpClient); + } + + @Test + public void processEventShouldSendEventWhenConsentIsValidButWasParsedFromUserExt() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().ext(ExtUser.builder().consent(VALID_CONSENT).build()).build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + any(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenVendorIsNotAllowed() { + // given + final User givenUser = User.builder().ext(ExtUser.builder().consent(CONSENT_WITHOUT_VENDOR).build()).build(); + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().user(givenUser).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenPurposeIsNotAllowed() { + // given + final User givenUser = User.builder().ext(ExtUser.builder().consent(CONSENT_WITHOUT_PURPOSE_9).build()).build(); + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().user(givenUser).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenAccountsDoesNotHaveConfiguredPublisher() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("unknown_publisherId", "anotherCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + + final AmpEvent ampEvent = AmpEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .bidRequest(BidRequest.builder().site(givenSite).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(ampEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendWhenAccountsHasConfiguredAppsOrSites() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("publisherId_bundleId", "accountCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + // given + final App givenApp = App.builder().bundle("bundleId") + .publisher(Publisher.builder().id("publisherId").build()).build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .app(givenApp) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + any(), + eq(expectedEventPayload), + eq(1000L)); + } + + @Test + public void processEventShouldSendWhenAccountsHasConfiguredAppsOrSitesOnly() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("_mySite", "accountCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + // given + final Site givenSite = Site.builder().id("mySite").build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + any(), + eq(expectedEventPayload), + eq(1000L)); + } + + @Test + public void processEventShouldSendEncodingGzipHeaderAndCompressedPayload() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(true) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("publisherId", "accountCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder().site(givenSite).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .site(givenSite) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + aryEq(gzip(expectedEventPayload)), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("content-encoding", "gzip"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + private static byte[] gzip(String value) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { + + gzip.write(value.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + + return obj.toByteArray(); + } catch (IOException e) { + return new byte[]{}; + } + } +} diff --git a/src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java new file mode 100644 index 00000000000..c58222f703b --- /dev/null +++ b/src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java @@ -0,0 +1,48 @@ +package org.prebid.server.analytics.reporter.agma; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EventBufferTest { + + @Test + public void pollToFlushShouldReturnEventsToFlushWhenMaxEventsExceeded() { + // given + final EventBuffer target = new EventBuffer<>(1, 999); + target.put("test", 4); + + // when and then + assertThat(target.pollToFlush()).containsExactly("test"); + } + + @Test + public void pollToFlushShouldReturnEventsToFlushWhenMaxBytesExceeded() { + // given + final EventBuffer target = new EventBuffer<>(999, 1); + target.put("test", 4); + + // when and then + assertThat(target.pollToFlush()).containsExactly("test"); + } + + @Test + public void pollToFlushShouldNotReturnAnyEventsWhenLimitsAreNotExceeded() { + // given + final EventBuffer target = new EventBuffer<>(999, 999); + target.put("test", 4); + + // when and then + assertThat(target.pollToFlush()).isEmpty(); + } + + @Test + public void pollAllShouldReturnAllEvents() { + // given + final EventBuffer target = new EventBuffer<>(999, 999); + target.put("test", 4); + + // when and then + assertThat(target.pollAll()).containsExactly("test"); + } +} diff --git a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java index 0e4e0e0e8fd..cccdbb4e149 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java @@ -1,6 +1,7 @@ package org.prebid.server.analytics.reporter.greenbids; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Banner; @@ -26,20 +27,43 @@ import org.prebid.server.VertxTest; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsBid; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode; import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRejectionReason; import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; import org.prebid.server.vertx.httpclient.HttpClient; @@ -50,6 +74,7 @@ import java.time.Clock; import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -113,6 +138,65 @@ public void setUp() { prebidVersionProvider); } + @Test + public void shouldReceiveValidResponseOnAuctionContextWithAnalyticsTagForBanner() throws IOException { + // given + final Banner banner = givenBanner(); + + final ObjectNode impExtNode = mapper.createObjectNode(); + impExtNode.set("gpid", TextNode.valueOf("gpidvalue")); + impExtNode.set("prebid", givenPrebidBidderParamsNode()); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(impExtNode) + .banner(banner) + .build(); + + final AuctionContext auctionContext = givenAuctionContextWithAnalyticsTag( + context -> context, List.of(imp), true, true); + final AuctionEvent event = AuctionEvent.builder() + .auctionContext(auctionContext) + .bidResponse(auctionContext.getBidResponse()) + .build(); + + final HttpClientResponse mockResponse = mock(HttpClientResponse.class); + when(mockResponse.getStatusCode()).thenReturn(202); + when(httpClient.post(anyString(), any(MultiMap.class), anyString(), anyLong())) + .thenReturn(Future.succeededFuture(mockResponse)); + final CommonMessage expectedCommonMessage = givenCommonMessageForBannerWithRtb2Imp(); + + // when + final Future result = target.processEvent(event); + + // then + assertThat(result.succeeded()).isTrue(); + verify(httpClient).post( + eq(greenbidsAnalyticsProperties.getAnalyticsServerUrl()), + headersCaptor.capture(), + jsonCaptor.capture(), + eq(greenbidsAnalyticsProperties.getTimeoutMs())); + + final String capturedJson = jsonCaptor.getValue(); + final CommonMessage capturedCommonMessage = jacksonMapper.mapper() + .readValue(capturedJson, CommonMessage.class); + + assertThat(capturedCommonMessage).usingRecursiveComparison() + .ignoringFields("billingId", "greenbidsId") + .isEqualTo(expectedCommonMessage); + assertThat(capturedCommonMessage.getGreenbidsId()).isNotNull(); + assertThat(capturedCommonMessage.getBillingId()).isNotNull(); + capturedCommonMessage.getAdUnits().forEach(adUnit -> { + assertThat(adUnit.getOrtb2ImpResult().getExt().getGreenbids().getFingerprint()).isNotNull(); + assertThat(adUnit.getOrtb2ImpResult().getExt().getTid()).isNotNull(); + }); + + assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER)) + .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString()); + assertThat(headersCaptor.getValue().get(HttpUtil.CONTENT_TYPE_HEADER)) + .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString()); + } + @Test public void shouldReceiveValidResponseOnAuctionContextForBanner() throws IOException { // given @@ -508,6 +592,28 @@ private static AuctionContext givenAuctionContext( return auctionContextCustomizer.apply(auctionContextBuilder).build(); } + private static AuctionContext givenAuctionContextWithAnalyticsTag( + UnaryOperator auctionContextCustomizer, + List imps, + boolean includeBidResponse, + boolean includeHookExecutionContextWithAnalyticsTag) { + final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .httpRequest(HttpRequestContext.builder().build()) + .bidRequest(givenBidRequest(request -> request, imps)) + .bidRejectionTrackers(Map.of("seat3", givenBidRejectionTracker())); + + if (includeHookExecutionContextWithAnalyticsTag) { + final HookExecutionContext hookExecutionContext = givenHookExecutionContextWithAnalyticsTag(); + auctionContextBuilder.hookExecutionContext(hookExecutionContext); + } + + if (includeBidResponse) { + auctionContextBuilder.bidResponse(givenBidResponse(response -> response)); + } + + return auctionContextCustomizer.apply(auctionContextBuilder).build(); + } + private static BidRequest givenBidRequest( UnaryOperator bidRequestCustomizer, List imps) { @@ -533,6 +639,37 @@ private static String givenUserAgent() { + "AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2"; } + private static HookExecutionContext givenHookExecutionContextWithAnalyticsTag() { + final ObjectNode analyticsResultNode = mapper.valueToTree( + singletonMap( + "adunitcodevalue", + createAnalyticsResultNode())); + + final ActivityImpl activity = ActivityImpl.of( + "greenbids-filter", + "success", + Collections.singletonList( + ResultImpl.of("success", analyticsResultNode, null))); + + final TagsImpl tags = TagsImpl.of(Collections.singletonList(activity)); + + final HookExecutionOutcome hookExecutionOutcome = HookExecutionOutcome.builder() + .hookId(HookId.of("greenbids-real-time-data", null)) + .analyticsTags(tags) + .build(); + + final GroupExecutionOutcome groupExecutionOutcome = GroupExecutionOutcome.of( + Collections.singletonList(hookExecutionOutcome)); + + final StageExecutionOutcome stageExecutionOutcome = StageExecutionOutcome.of( + "auction-request", Collections.singletonList(groupExecutionOutcome)); + + final EnumMap> stageOutcomes = new EnumMap<>(Stage.class); + stageOutcomes.put(Stage.processed_auction_request, Collections.singletonList(stageExecutionOutcome)); + + return HookExecutionContext.of(null, stageOutcomes); + } + private static BidResponse givenBidResponse(UnaryOperator bidResponseCustomizer) { return bidResponseCustomizer.apply(BidResponse.builder() .id("response1") @@ -546,10 +683,77 @@ private static BidResponse givenBidResponse(UnaryOperator bidResponseCustomizer) { + final ObjectNode analyticsResultNode = mapper.valueToTree( + singletonMap( + "adunitcodevalue", + createAnalyticsResultNode())); + + final ExtModulesTraceAnalyticsTags analyticsTags = ExtModulesTraceAnalyticsTags.of( + Collections.singletonList( + ExtModulesTraceAnalyticsActivity.of( + null, null, Collections.singletonList( + ExtModulesTraceAnalyticsResult.of( + null, analyticsResultNode, null))))); + + final ExtModulesTraceInvocationResult invocationResult = ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("greenbids-real-time-data", null)) + .analyticsTags(analyticsTags) + .build(); + + final ExtModulesTraceStageOutcome outcome = ExtModulesTraceStageOutcome.of( + "auction-request", null, + Collections.singletonList(ExtModulesTraceGroup.of( + null, Collections.singletonList(invocationResult)))); + + final ExtModulesTraceStage stage = ExtModulesTraceStage.of( + Stage.processed_auction_request, null, + Collections.singletonList(outcome)); + + final ExtModulesTrace modulesTrace = ExtModulesTrace.of(null, Collections.singletonList(stage)); + + final ExtModules modules = ExtModules.of(null, null, modulesTrace); + + final ExtBidResponsePrebid prebid = ExtBidResponsePrebid.builder().modules(modules).build(); + + final ExtBidResponse extBidResponse = ExtBidResponse.builder().prebid(prebid).build(); + + return bidResponseCustomizer.apply(BidResponse.builder() + .id("response2") + .seatbid(List.of( + givenSeatBid( + seatBid -> seatBid.seat("seat1"), + bid -> bid.id("bid1").price(BigDecimal.valueOf(1.5))), + givenSeatBid( + seatBid -> seatBid.seat("seat2"), + bid -> bid.id("bid2").price(BigDecimal.valueOf(0.5))))) + .cur("USD") + .ext(extBidResponse)).build(); + } + + private static ObjectNode createAnalyticsResultNode() { + final ObjectNode keptInAuctionNode = new ObjectNode(JsonNodeFactory.instance); + keptInAuctionNode.put("seat1", true); + keptInAuctionNode.put("seat2", true); + keptInAuctionNode.put("seat3", true); + + final ObjectNode explorationResultNode = new ObjectNode(JsonNodeFactory.instance); + explorationResultNode.put("fingerprint", "4f8d2e76-87fe-47c7-993f-d905b5fe2aa7"); + explorationResultNode.set("keptInAuction", keptInAuctionNode); + explorationResultNode.put("isExploration", false); + + final ObjectNode analyticsResultNode = new ObjectNode(JsonNodeFactory.instance); + analyticsResultNode.set("greenbids", explorationResultNode); + analyticsResultNode.put("tid", "c65c165d-f4ea-4301-bb91-982ce813dd3e"); + + return analyticsResultNode; + } + private static SeatBid givenSeatBid(UnaryOperator seatBidCostumizer, UnaryOperator... bidCustomizers) { return seatBidCostumizer.apply(SeatBid.builder() - .bid(givenBids(bidCustomizers))).build(); + .bid(givenBids(bidCustomizers))).build(); } private static List givenBids(UnaryOperator... bidCustomizers) { @@ -569,7 +773,7 @@ private static BidRejectionTracker givenBidRejectionTracker() { "seat3", Set.of("adunitcodevalue"), 1.0); - bidRejectionTracker.reject("imp1", BidRejectionReason.NO_BID); + bidRejectionTracker.rejectImp("imp1", BidRejectionReason.NO_BID); return bidRejectionTracker; } @@ -621,6 +825,16 @@ private static CommonMessage expectedCommonMessageForBanner() { .bids(expectedGreenbidBids())); } + private static CommonMessage givenCommonMessageForBannerWithRtb2Imp() { + return expectedCommonMessage( + adUnit -> adUnit + .code("adunitcodevalue") + .unifiedCode(GreenbidsUnifiedCode.of("gpidvalue", "gpidSource")) + .mediaTypes(MediaTypes.of(givenExtBanner(320, 50, null), null, null)) + .bids(expectedGreenbidBids()) + .ortb2ImpResult(givenOrtb2Imp())); + } + private static CommonMessage expectedCommonMessageForVideo() { return expectedCommonMessage( adUnit -> adUnit @@ -718,4 +932,17 @@ private static Video givenVideo() { .plcmt(1) .build(); } + + private static Ortb2ImpResult givenOrtb2Imp() { + return Ortb2ImpResult.of( + Ortb2ImpExtResult.of( + ExplorationResult.of( + "4f8d2e76-87fe-47c7-993f-d905b5fe2aa7", + Map.of("seat1", true, "seat2", true, "seat3", true), + false + ), + "c65c165d-f4ea-4301-bb91-982ce813dd3e" + ) + ); + } } diff --git a/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java index 4fc744e4b43..b7418e7b777 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java @@ -16,7 +16,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import org.springframework.test.util.ReflectionTestUtils; diff --git a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java index d1de6726c42..3c05d00af37 100644 --- a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java +++ b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java @@ -18,8 +18,8 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 13944033ed1..19181bf3503 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -20,8 +20,6 @@ import com.iab.openrtb.response.Response; import com.iab.openrtb.response.SeatBid; import io.vertx.core.Future; -import lombok.Value; -import lombok.experimental.Accessors; import org.apache.commons.collections4.MapUtils; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; @@ -62,12 +60,12 @@ import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; -import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.identity.IdGenerator; import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; @@ -82,6 +80,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; @@ -112,6 +111,7 @@ import org.prebid.server.settings.model.AccountAuctionEventConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.VideoStoredDataResult; +import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.vast.VastModifier; import java.math.BigDecimal; @@ -155,6 +155,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @@ -189,6 +190,8 @@ public class BidResponseCreatorTest extends VertxTest { private ActivityInfrastructure activityInfrastructure; @Mock(strictness = LENIENT) private CacheTtl mediaTypeCacheTtl; + @Mock(strictness = LENIENT) + private CacheDefaultTtlProperties cacheDefaultProperties; @Spy private WinningBidComparatorFactory winningBidComparatorFactory; @@ -208,6 +211,11 @@ public void setUp() { given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(null); given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(null); + given(cacheDefaultProperties.getBannerTtl()).willReturn(null); + given(cacheDefaultProperties.getVideoTtl()).willReturn(null); + given(cacheDefaultProperties.getAudioTtl()).willReturn(null); + given(cacheDefaultProperties.getNativeTtl()).willReturn(null); + given(categoryMappingService.createCategoryMapping(any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture( CategoryMappingResult.of(emptyMap(), emptyMap(), invocationOnMock.getArgument(0), null))); @@ -1223,7 +1231,6 @@ public void shouldReturnEmptyAssetIfNoRelatedNativeAssetFound() throws JsonProce final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - assertThat(bidResponse.getSeatbid()).hasSize(1) .flatExtracting(SeatBid::getBid) .extracting(Bid::getAdm) @@ -1283,7 +1290,6 @@ public void shouldReturnEmptyAssetIfIdIsNotPresentRelatedNativeAssetFound() thro final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - assertThat(bidResponse.getSeatbid()).hasSize(1) .flatExtracting(SeatBid::getBid) .extracting(Bid::getAdm) @@ -1553,7 +1559,7 @@ public void shouldPopulateTargetingKeywords() { final AuctionContext auctionContext = givenAuctionContext( givenBidRequest( - identity(), + request -> request.app(App.builder().build()), extBuilder -> extBuilder.targeting(givenTargeting()), givenImp()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); @@ -1571,7 +1577,45 @@ public void shouldPopulateTargetingKeywords() { tuple("hb_pb", "5.00"), tuple("hb_pb_bidder1", "5.00"), tuple("hb_bidder", "bidder1"), - tuple("hb_bidder_bidder1", "bidder1")); + tuple("hb_bidder_bidder1", "bidder1"), + tuple("hb_env", "mobile-app"), + tuple("hb_env_bidder1", "mobile-app")); + + verify(coreCacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); + } + + @Test + public void shouldPopulateTargetingKeywordsForAmpRequest() { + // given + final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build(); + final List bidderResponses = singletonList(BidderResponse.of("bidder1", + givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest( + request -> request.app(App.builder().build()), + extBuilder -> extBuilder + .targeting(givenTargeting()) + .amp(ExtRequestPrebidAmp.of(Map.of("key", "value"))), + givenImp()), + contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); + + // when + final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); + + // then + assertThat(bidResponse.getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1) + .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting()) + .flatExtracting(Map::entrySet) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("hb_pb", "5.00"), + tuple("hb_pb_bidder1", "5.00"), + tuple("hb_bidder", "bidder1"), + tuple("hb_bidder_bidder1", "bidder1"), + tuple("hb_env", "amp"), + tuple("hb_env_bidder1", "amp")); verify(coreCacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); } @@ -1603,7 +1647,8 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() { 20, clock, jacksonMapper, - mediaTypeCacheTtl); + mediaTypeCacheTtl, + cacheDefaultProperties); // when final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); @@ -3530,7 +3575,7 @@ public void shouldDropFledgeResponsesReferencingUnknownImps() { public void shouldPopulateExtPrebidSeatNonBidWhenReturnAllBidStatusFlagIsTrue() { // given final BidRejectionTracker bidRejectionTracker = mock(BidRejectionTracker.class); - given(bidRejectionTracker.getRejectionReasons()).willReturn(singletonMap("impId2", BidRejectionReason.NO_BID)); + given(bidRejectionTracker.getRejectedImps()).willReturn(singletonMap("impId2", BidRejectionReason.NO_BID)); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(3.67)).impid("impId").build(); final List bidderResponses = singletonList( @@ -3770,7 +3815,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { final Imp imp = Imp.builder().id("impId").exp(20).build(); final List bidderResponses = asList(BidderResponse.of( "bidder1", - givenSeatBid(BidderBid.of(bid, banner, "USD")), + givenSeatBid(BidderBid.of(bid, video, "USD")), 100)); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() @@ -3778,17 +3823,18 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(30) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .events(mapper.createObjectNode()) - .build())), imp), + .events(mapper.createObjectNode()) + .build())), imp), builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3797,6 +3843,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3818,6 +3869,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(10); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(10); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -3832,7 +3884,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { final Imp imp = Imp.builder().id("impId").exp(20).build(); final List bidderResponses = asList(BidderResponse.of( "bidder1", - givenSeatBid(BidderBid.of(bid, banner, "USD")), + givenSeatBid(BidderBid.of(bid, video, "USD")), 100)); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() @@ -3840,7 +3892,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(30) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -3850,7 +3902,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3859,6 +3912,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3880,6 +3938,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(20); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(20); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -3894,7 +3953,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { final Imp imp = Imp.builder().id("impId").exp(null).build(); final List bidderResponses = asList(BidderResponse.of( "bidder1", - givenSeatBid(BidderBid.of(bid, banner, "USD")), + givenSeatBid(BidderBid.of(bid, video, "USD")), 100)); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() @@ -3902,7 +3961,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(30) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -3912,7 +3971,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3921,6 +3981,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3931,7 +3996,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { assertThat(response.succeeded()).isTrue(); assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) - .containsExactly(30); + .containsExactly(31); verify(coreCacheService).cacheBidsOpenrtb( bidsArgumentCaptor.capture(), @@ -3942,6 +4007,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(30); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(31); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -3950,7 +4016,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { } @Test - public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtl() { + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtlForBannerBid() { // given final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); final Imp imp = Imp.builder().id("impId").exp(null).build(); @@ -3964,7 +4030,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(null) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -3974,7 +4040,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3983,6 +4050,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3993,7 +4065,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne assertThat(response.succeeded()).isTrue(); assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) - .containsExactly(60); + .containsExactly(40); verify(coreCacheService).cacheBidsOpenrtb( bidsArgumentCaptor.capture(), @@ -4003,7 +4075,77 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); - assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(60); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(40); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountVideoTtlForVideoBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, video, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(41); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(41); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(41); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4012,7 +4154,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne } @Test - public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl() { + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtlForBannerBid() { // given final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); final Imp imp = Imp.builder().id("impId").exp(null).build(); @@ -4026,7 +4168,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(null) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(null) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -4037,6 +4179,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl .id("accountId") .auction(AccountAuctionConfig.builder() .bannerCacheTtl(null) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -4045,6 +4188,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -4066,6 +4214,352 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(50); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtlForVideoBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, video, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(null) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(51); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(51); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(51); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForBannerBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, banner, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(null) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(null); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(60); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(60); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForVideoBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, video, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(null) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(null); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(61); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(61); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(61); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForAudioBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, audio, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(62); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(62); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForNativeBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, xNative, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(63); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(63); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4240,7 +4734,7 @@ public void createShouldSendCacheRequestWithVideoBidWithTtlMaxOfTtlAndVideoTtl() final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsExactly(30); - assertThat(capturedBidInfo).extracting(BidInfo::getVideoTtl).containsExactly(40); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsExactly(40); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4299,7 +4793,7 @@ public void createShouldSendCacheRequestWithBannerBidWithTtlMaxOfTtlAndVideoTtl( final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsExactly(30); - assertThat(capturedBidInfo).extracting(BidInfo::getVideoTtl).containsOnlyNulls(); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnlyNulls(); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4539,7 +5033,8 @@ private BidResponseCreator givenBidResponseCreator(int truncateAttrChars) { truncateAttrChars, clock, jacksonMapper, - mediaTypeCacheTtl); + mediaTypeCacheTtl, + cacheDefaultProperties); } private static String toTargetingByKey(Bid bid, String targetingKey) { @@ -4567,11 +5062,4 @@ private static ObjectNode extWithTargeting(String targetBidderCode, Map List mutableList(T... values) { return Arrays.stream(values).collect(Collectors.toCollection(ArrayList::new)); } - - @Accessors(fluent = true) - @Value(staticConstructor = "of") - private static class BidderResponsePayloadImpl implements BidderResponsePayload { - - List bids; - } } diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java new file mode 100644 index 00000000000..9bfbc9cb143 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -0,0 +1,360 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.when; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; + +@ExtendWith(MockitoExtension.class) +public class BidsAdjusterTest extends VertxTest { + + @Mock(strictness = LENIENT) + private ResponseBidValidator responseBidValidator; + + @Mock(strictness = LENIENT) + private PriceFloorEnforcer priceFloorEnforcer; + + @Mock(strictness = LENIENT) + private DsaEnforcer dsaEnforcer; + + @Mock(strictness = LENIENT) + private BidAdjustmentsProcessor bidAdjustmentsProcessor; + + private BidsAdjuster target; + + @BeforeEach + public void setUp() { + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); + + given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); + given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + .willAnswer(inv -> inv.getArgument(0)); + + target = new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); + } + + @Test + public void shouldReturnBidsAdjustedByBidAdjustmentsProcessor() { + // given + final BidderBid bidToAdjust = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder().bids(List.of(bidToAdjust)).build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + List.of(givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + identity()); + + final BidderBid adjustedBid = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.TEN).build(), "USD"); + + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(adjustedBid)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target.validateAndAdjustBids( + auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(adjustedBid); + } + + @Test + public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { + // given + final BidderBid bidToAccept = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderBid bidToReject = + givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.TEN).build(), "USD"); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(bidToAccept, bidToReject)) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(List.of( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), + identity()); + + given(priceFloorEnforcer.enforce(any(), any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target.validateAndAdjustBids( + auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(bidToAccept); + } + + @Test + public void shouldReturnBidsAcceptedByDsaEnforcer() { + // given + final BidderBid bidToAccept = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderBid bidToReject = + givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.TEN).build(), "USD"); + + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(bidToAccept, bidToReject)) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(List.of( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), + identity()); + + given(dsaEnforcer.enforce(any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(bidToAccept); + } + + @Test + public void shouldTolerateResponseBidValidationErrors() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(givenBidderBid( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + when(responseBidValidator.validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.error("Error: bid validation error.")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .isEmpty(); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly( + BidderError.invalidBid( + "BidId `bidId1` validation messages: Error: Error: bid validation error.")); + } + + @Test + public void shouldTolerateResponseBidValidationWarnings() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(givenBidderBid( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + when(responseBidValidator.validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.warning(singletonList("Error: bid validation warning."))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .hasSize(1); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly(BidderError.invalidBid( + "BidId `bidId1` validation messages: Warning: Error: bid validation warning.")); + } + + @Test + public void shouldAddWarningAboutMultipleCurrency() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(BigDecimal.valueOf(2.0)).build(), + "CUR1"))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2"))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result).hasSize(1); + + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + final BidderError expectedWarning = BidderError.badInput( + "a single currency (CUR1) has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency."); + assertThat(firstSeatBid.getWarnings()).containsOnly(expectedWarning); + } + + private List givenAuctionParticipation( + BidderResponse bidderResponse, BidRequest bidRequest) { + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidRequest(bidRequest) + .build(); + + return List.of(AuctionParticipation.builder() + .bidder("bidder") + .bidderRequest(bidderRequest) + .bidderResponse(bidderResponse) + .build()); + } + + private AuctionContext givenAuctionContext(BidRequest bidRequest) { + return AuctionContext.builder() + .bidRequest(bidRequest) + .bidRejectionTrackers(Map.of("bidder", new BidRejectionTracker("bidder", Set.of(), 1))) + .build(); + } + + private static BidRequest givenBidRequest(List imp, + UnaryOperator bidRequestBuilderCustomizer) { + + return bidRequestBuilderCustomizer + .apply(BidRequest.builder().cur(singletonList("USD")).imp(imp).tmax(500L)) + .build(); + } + + private static Imp givenImp(T ext, UnaryOperator impBuilderCustomizer) { + return impBuilderCustomizer.apply(Imp.builder() + .id(UUID.randomUUID().toString()) + .ext(mapper.valueToTree(singletonMap( + "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap())))) + .build(); + } + + private static BidderBid givenBidderBid(Bid bid) { + return BidderBid.of(bid, banner, null); + } + + private static BidderBid givenBidderBid(Bid bid, String currency) { + return BidderBid.of(bid, banner, currency); + } +} diff --git a/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java b/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java index 131a726647f..5254ea4edda 100644 --- a/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java +++ b/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java @@ -104,7 +104,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsNotRequiredAndDsaRespons .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -137,7 +137,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsNotRequiredAndDsaRespons .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -333,7 +333,7 @@ public void enforceShouldRejectBidAndAddWarningWhenBidExtHasEmptyDsaAndDsaIsRequ .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -367,7 +367,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndDsaResponseHa .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -401,7 +401,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndDsaResponseHa .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -436,7 +436,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherAndA .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -471,7 +471,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherAndA .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -505,7 +505,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherNotR .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } private static ExtRegs givenExtRegs(DsaRequired dsaRequired, DsaPublisherRender pubRender) { diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 1f72014ea59..bfecf6632f9 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -25,7 +25,6 @@ import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -43,11 +42,12 @@ import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; @@ -68,13 +68,11 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.ExecutionAction; @@ -86,13 +84,13 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; -import org.prebid.server.hooks.v1.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.AppliedToImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.log.CriteriaLogManager; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; @@ -111,7 +109,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; @@ -126,7 +123,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.proto.openrtb.ext.request.TraceLevel; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; @@ -154,8 +150,6 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.Ortb; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.io.IOException; import java.math.BigDecimal; @@ -186,6 +180,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -193,7 +188,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; @@ -202,15 +196,11 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; -import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @ExtendWith(MockitoExtension.class) public class ExchangeServiceTest extends VertxTest { @@ -230,6 +220,9 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private SupplyChainResolver supplyChainResolver; + @Mock(strictness = LENIENT) + private ImpAdjuster impAdjuster; + @Mock private DebugResolver debugResolver; @@ -251,12 +244,6 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private HttpBidderRequester httpBidderRequester; - @Mock(strictness = LENIENT) - private ResponseBidValidator responseBidValidator; - - @Mock(strictness = LENIENT) - private CurrencyConversionService currencyService; - @Mock(strictness = LENIENT) private BidResponseCreator bidResponseCreator; @@ -272,17 +259,11 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private PriceFloorAdjuster priceFloorAdjuster; - @Mock(strictness = LENIENT) - private PriceFloorEnforcer priceFloorEnforcer; - @Mock(strictness = LENIENT) private PriceFloorProcessor priceFloorProcessor; @Mock(strictness = LENIENT) - private DsaEnforcer dsaEnforcer; - - @Mock(strictness = LENIENT) - private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private BidsAdjuster bidsAdjuster; @Mock private Metrics metrics; @@ -325,6 +306,7 @@ public void setUp() { null, null, 0, + null, false, false, CompressionType.NONE, @@ -349,6 +331,8 @@ public void setUp() { given(fpdResolver.resolveImpExt(any(), anyBoolean())) .willAnswer(invocation -> invocation.getArgument(0)); + given(impAdjuster.adjust(any(), any(), any(), any())).willAnswer(invocation -> invocation.getArgument(0)); + given(supplyChainResolver.resolveForBidder(anyString(), any())).willReturn(null); given(hookStageExecutor.executeBidderRequestStage(any(), any())) @@ -365,14 +349,12 @@ public void setUp() { false, AuctionResponsePayloadImpl.of(invocation.getArgument(0))))); + given(bidsAdjuster.validateAndAdjustBids(any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(mediaTypeProcessor.process(any(), anyString(), any(), any())) .willAnswer(invocation -> MediaTypeProcessingResult.succeeded(invocation.getArgument(0), emptyList())); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - given(uidUpdater.updateUid(any(), any(), any())) .willAnswer(inv -> Optional.ofNullable((AuctionContext) inv.getArgument(1)) .map(AuctionContext::getBidRequest) @@ -388,14 +370,11 @@ public void setUp() { given(storedResponseProcessor.updateStoredBidResponse(any())) .willAnswer(inv -> inv.getArgument(0)); - given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); - given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); given(priceFloorAdjuster.adjustForImp(any(), any(), any(), any(), any())) .willAnswer(inv -> Price.of( ((Imp) inv.getArgument(0)).getBidfloorcur(), ((Imp) inv.getArgument(0)).getBidfloor())); - given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null); given(priceFloorProcessor.enrichWithPriceFloors(any(), any(), any(), any(), any())) .willAnswer(inv -> inv.getArgument(0)); @@ -496,14 +475,20 @@ public void shouldExtractRequestWithBidderSpecificExtension() { // given givenBidder(givenEmptySeatBid()); - final BidRequest bidRequest = givenBidRequest(singletonList( - givenImp(singletonMap("someBidder", 1), builder -> builder - .id("impId") - .banner(Banner.builder() - .format(singletonList(Format.builder().w(400).h(300).build())) - .build()))), + final Imp givenImp = givenImp(singletonMap("someBidder", 1), builder -> builder + .id("impId") + .banner(Banner.builder() + .format(singletonList(Format.builder().w(400).h(300).build())) + .build())); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp), builder -> builder.id("requestId").tmax(500L)); + final ObjectNode adjustedExt = givenImp.getExt().deepCopy(); + final Imp adjustedImp = givenImp.toBuilder().ext(adjustedExt).build(); + given(impAdjuster.adjust(any(), any(), any(), any())).willReturn(adjustedImp); + // when target.holdAuction(givenRequestContext(bidRequest)); @@ -521,6 +506,15 @@ public void shouldExtractRequestWithBidderSpecificExtension() { .build())) .tmax(500L) .build()); + + final ArgumentCaptor impCaptor = forClass(Imp.class); + verify(impAdjuster).adjust(impCaptor.capture(), eq("someBidder"), any(), any()); + + final Imp actualImp = impCaptor.getValue(); + assertThat(actualImp).isNotSameAs(givenImp); + assertThat(actualImp).isEqualTo(givenImp); + assertThat(actualImp.getExt()).isNotSameAs(givenImp.getExt()); + assertThat(actualImp.getExt()).isEqualTo(givenImp.getExt()); } @Test @@ -1488,12 +1482,10 @@ public void shouldCallBidResponseCreatorWithExpectedParamsAndUpdateDebugErrors() verify(bidResponseCreator) .create(contextArgumentCaptor.capture(), eq(expectedCacheInfo), eq(expectedMultiBidMap)); - final ObjectNode expectedBidExt = mapper.createObjectNode().put("origbidcpm", new BigDecimal("7.89")); final Bid expectedThirdBid = Bid.builder() .id("bidId3") .impid("impId3") .price(BigDecimal.valueOf(7.89)) - .ext(expectedBidExt) .build(); final List auctionParticipations = contextArgumentCaptor.getValue().getAuctionParticipations(); @@ -1631,81 +1623,6 @@ public void shouldTolerateNullRequestExtPrebidTargeting() { .allSatisfy(map -> assertThat(map).isNull()); } - @Test - public void shouldTolerateResponseBidValidationErrors() { - // given - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .auctiontimestamp(1000L) - .build()))); - - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.error( - singletonList("bid validation warning"), - "bid validation error")); - - givenBidResponseCreator(singletonList(Bid.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .isEmpty(); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly( - BidderError.invalidBid("BidId `bidId1` validation messages: Error: bid validation error." - + " Warning: bid validation warning")); - } - - @Test - public void shouldTolerateResponseBidValidationWarnings() { - // given - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .auctiontimestamp(1000L) - .build()))); - - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success( - singletonList("bid validation warning"))); - - givenBidResponseCreator(singletonList(Bid.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .hasSize(1); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(BidderError.invalidBid( - "BidId `bidId1` validation messages: Warning: bid validation warning")); - } - @Test public void shouldRejectBidIfCurrencyIsNotValid() { // given @@ -1720,9 +1637,6 @@ public void shouldRejectBidIfCurrencyIsNotValid() { .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any(), any(), any(), any())) - .willReturn(ValidationResult.error("BidResponse currency is not valid: USDD")); - final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), "BidResponse currency is not valid: USDD")); givenBidResponseCreator(singletonMap("bidder1", bidderErrors)); @@ -2147,7 +2061,7 @@ public void shouldPassUserDataAndExtDataOnlyForAllowedBidder() { final ObjectNode dataNode = mapper.createObjectNode().put("data", "value"); final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0); - final List eids = singletonList(Eid.of("eId", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("eId").uids(emptyList()).build()); final ExtUser extUser = ExtUser.builder().data(dataNode).build(); final List data = singletonList(Data.builder().build()); @@ -2196,50 +2110,44 @@ public void shouldPassUserDataAndExtDataOnlyForAllowedBidder() { public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceIgnoringCase() { testUserEidsPermissionFiltering( // given - asList( - Eid.of("source1", null, null), - Eid.of("source2", null, null)), + asList(Eid.builder().source("source1").build(), Eid.builder().source("source2").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("OtHeRbIdDeR"))), emptyMap(), // expected - singletonList(Eid.of("source2", null, null)) - ); + singletonList(Eid.builder().source("source2").build())); } @Test public void shouldNotFilterUserExtEidsWhenEidsPermissionDoesNotContainSourceIgnoringCase() { testUserEidsPermissionFiltering( // given - singletonList(Eid.of("source1", null, null)), + singletonList(Eid.builder().source("source1").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source2", singletonList("OtHeRbIdDeR"))), emptyMap(), // expected - singletonList(Eid.of("source1", null, null)) - ); + singletonList(Eid.builder().source("source1").build())); } @Test public void shouldNotFilterUserExtEidsWhenSourceAllowedForAllBiddersIgnoringCase() { testUserEidsPermissionFiltering( // given - singletonList(Eid.of("source1", null, null)), + singletonList(Eid.builder().source("source1").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("*"))), emptyMap(), // expected - singletonList(Eid.of("source1", null, null)) - ); + singletonList(Eid.builder().source("source1").build())); } @Test public void shouldNotFilterUserExtEidsWhenSourceAllowedForBidderIgnoringCase() { testUserEidsPermissionFiltering( // given - singletonList(Eid.of("source1", null, null)), + singletonList(Eid.builder().source("source1").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("SoMeBiDdEr"))), emptyMap(), // expected - singletonList(Eid.of("source1", null, null)) - ); + singletonList(Eid.builder().source("source1").build())); } @Test @@ -2257,7 +2165,7 @@ public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceAndSetNullIfNo singletonList("otherBidder"))))) .build())) .user(User.builder() - .eids(singletonList(Eid.of("source1", null, null))) + .eids(singletonList(Eid.builder().source("source1").build())) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build())); @@ -2293,7 +2201,7 @@ public void shouldFilterUserExtEidsWhenBidderPermissionsGivenToBidderAliasOnly() singletonList("someBidderAlias"))))) .build())) .user(User.builder() - .eids(singletonList(Eid.of("source1", null, null))) + .eids(singletonList(Eid.builder().source("source1").build())) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build())); @@ -2329,7 +2237,7 @@ public void shouldFilterUserExtEidsWhenPermissionsGivenToBidderButNotForAlias() singletonList("someBidder"))))) .build())) .user(User.builder() - .eids(singletonList(Eid.of("source1", null, null))) + .eids(singletonList(Eid.builder().source("source1").build())) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build())); @@ -2393,7 +2301,7 @@ public void shouldMaskUserExtIfDataBiddersListIsEmpty() { final ObjectNode dataNode = mapper.createObjectNode().put("data", "value"); final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0); - final List eids = singletonList(Eid.of("eId", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("eId").uids(emptyList()).build()); final ExtUser extUser = ExtUser.builder().data(dataNode).build(); final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr), @@ -3029,859 +2937,175 @@ public void holdAuctionShouldFailWhenSiteAppAndDoohArePresentInBidRequestAndStri } @Test - public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { + public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsService() { // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + final BigDecimal price = BigDecimal.valueOf(2.0); + givenBidder(BidderSeatBid.of( + singletonList(BidderBid.of( + Bid.builder().id("bidId").impid("impId").price(price) + .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build(), banner, null)))); - givenBidResponseCreator(singletonList(Bid.builder().price(updatedPrice).build())); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), + bidRequestBuilder -> bidRequestBuilder.app(App.builder() + .publisher(Publisher.builder().id("1001").build()).build())); // when final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); // then - assertThat(result.getBidResponse().getSeatbid()) + assertThat(result.getBidResponse().getSeatbid()).hasSize(1) .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice).containsExactly(updatedPrice); + .extracting(bid -> toExtBidPrebid(bid.getExt()).getEvents()) + .containsNull(); } @Test - public void shouldApplyStoredBidResponseAdjustments() { + public void shouldIncrementCommonMetrics() { // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) + .willReturn(Future.succeededFuture(givenSeatBid(singletonList( + givenBidderBid(Bid.builder().impid("impId").price(TEN).build()))))); - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(TEN); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someAlias", 1)), + builder -> builder + .site(Site.builder().publisher(Publisher.builder().id("accountId").build()).build()) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("someAlias", "someBidder")) + .build()))); // when target.holdAuction(givenRequestContext(bidRequest)); // then - verify(storedResponseProcessor).updateStoredBidResponse(any()); + verify(metrics).updateDebugRequestMetrics(false); + verify(metrics).updateAccountDebugRequestMetrics(any(), eq(false)); + verify(metrics).updateRequestBidderCardinalityMetric(1); + verify(metrics).updateAccountRequestMetrics(any(), eq(MetricName.openrtb2web)); + verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics( + eq("someBidder"), eq(MetricName.openrtb2web), eq(true)); + verify(metrics).updateAdapterResponseTime(eq("someBidder"), any(), anyInt()); + verify(metrics).updateAdapterRequestGotbidsMetrics(eq("someBidder"), any()); + verify(metrics).updateAdapterBidMetrics( + eq("someBidder"), any(), eq(10000L), eq(false), eq("banner")); } @Test - public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { + public void shouldCallUpdateCookieMetricsWithExpectedValue() { // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - // returns the same price as in argument - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), + builder -> builder.app(App.builder().build())); // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); + target.holdAuction(givenRequestContext(bidRequest)); // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice).containsExactly(BigDecimal.ONE); + verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics( + eq("someBidder"), eq(MetricName.openrtb2web), eq(false)); } @Test - public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { + public void shouldUseEmptyStringIfPublisherIdIsEmpty() { // given - final Bidder bidder = mock(Bidder.class); - final List bids = List.of( - Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), - Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), - Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), - Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); - final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); - - givenBidder("bidder", bidder, seatBid); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - final AuctionContext givenContext = givenRequestContext(bidRequest); + given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) + .willReturn(Future.succeededFuture(givenSeatBid(singletonList( + givenBidderBid(Bid.builder().price(TEN).build()))))); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); + final Account account = Account.builder().id("").build(); // when - final AuctionContext result = target.holdAuction(givenContext).result(); + target.holdAuction(givenRequestContext(bidRequest, account)); // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid).hasSize(1); - assertThat(givenContext.getDebugWarnings()) - .containsExactlyInAnyOrder( - "Dropped bid 'invalid_bid_1'. Does not contain a positive (or zero if there is a deal) 'price'", - "Dropped bid 'invalid_bid_2'. Does not contain a positive (or zero if there is a deal) 'price'", - "Dropped bid 'invalid_bid_3'. Does not contain a positive (or zero if there is a deal) 'price'" - ); - verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); + verify(metrics).updateAccountRequestMetrics(eq(account), eq(MetricName.openrtb2web)); } @Test - public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { + public void shouldIncrementNoBidRequestsMetric() { // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2.0)).build(), "CUR")))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); + given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) + .willReturn(Future.succeededFuture(givenSeatBid(emptyList()))); - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); // when target.holdAuction(givenRequestContext(bidRequest)); // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD"); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).isEmpty(); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + verify(metrics).updateAdapterRequestNobidMetrics(eq("someBidder"), any()); } @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() { + public void shouldIncrementGotBidsAndErrorMetricsIfBidderReturnsBidAndDifferentErrors() { // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", TEN); - - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); + given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) + .willReturn(Future.succeededFuture(BidderSeatBid.builder() + .bids(singletonList(givenBidderBid(Bid.builder().impid("impId").price(TEN).build()))) + .errors(asList( + // two identical errors to verify corresponding metric is submitted only once + BidderError.badInput("rubicon error"), + BidderError.badInput("rubicon error"), + BidderError.badServerResponse("rubicon error"), + BidderError.failedToRequestBids("rubicon failed to request bids"), + BidderError.timeout("timeout error"), + BidderError.generic("timeout error"))) + .build())); - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(TEN); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); // when target.holdAuction(givenRequestContext(bidRequest)); // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BigDecimal updatedPrice = BigDecimal.valueOf(100); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).isEmpty(); + verify(metrics).updateAdapterRequestGotbidsMetrics(eq("someBidder"), any()); + verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.badinput)); + verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.badserverresponse)); + verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.failedtorequestbids)); + verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.timeout)); + verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.unknown_error)); } @Test - public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { + public void shouldPassResponseToPostProcessor() { // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); - givenBidder("bidder", mock(Bidder.class), givenSeatBid(asList( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), - givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2")))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) - .willThrow( - new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); + final BidRequest bidRequest = givenBidRequest(emptyList()); // when target.holdAuction(givenRequestContext(bidRequest)); // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "CUR1"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1"); - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); - - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + verify(bidResponsePostProcessor).postProcess( + any(), + same(uidsCookie), + same(bidRequest), + any(), + eq(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build())); } @Test - public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { + public void shouldReturnBidResponseModifiedByAuctionResponseHooks() { // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD")))); - givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId2").price(BigDecimal.valueOf(10.0)).build(), "CUR")))); + given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) + .willReturn(Future.succeededFuture(givenSeatBid(emptyList()))); - final BidRequest bidRequest = BidRequest.builder().cur(singletonList("BAD")) - .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), - identity()))).build(); + doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + AuctionResponsePayloadImpl.of(BidResponse.builder().id("bidResponseId").build())))) + .when(hookStageExecutor).executeAuctionResponseStage(any(), any()); - final BigDecimal updatedPrice = BigDecimal.valueOf(20); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD"))) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2))); // when - target.holdAuction(givenRequestContext(bidRequest)); + final AuctionContext auctionContext = target.holdAuction(givenRequestContext(bidRequest)).result(); // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD")); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD")); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(2); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "USD"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD"); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsOnly(expectedBidderBid); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD"); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCurrency() { - // given - final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); - givenBidder("bidder", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(bidderPrice).build(), "USD")))); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.cur(asList("CUR1", "CUR2", "CUR2"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1")); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BidderError expectedError = BidderError.badInput("Cur parameter contains more than one currency." - + " CUR1 will be used"); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { - // given - final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); - final BigDecimal bidder2Price = BigDecimal.valueOf(2); - final BigDecimal bidder3Price = BigDecimal.valueOf(3); - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR")))); - givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP")))); - givenBidder("bidder3", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD")))); - - final Map impBidders = new HashMap<>(); - impBidders.put("bidder1", 1); - impBidders.put("bidder2", 2); - impBidders.put("bidder3", 3); - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(impBidders, identity())), builder -> builder.cur(singletonList("USD"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); - verifyNoMoreInteractions(currencyService); - - assertThat(contextArgumentCaptor.getValue().getAuctionParticipations()) - .hasSize(3) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsOnly(bidder3Price, updatedPrice, updatedPrice); - } - - @Test - public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsService() { - // given - final BigDecimal price = BigDecimal.valueOf(2.0); - givenBidder(BidderSeatBid.of( - singletonList(BidderBid.of( - Bid.builder().id("bidId").impid("impId").price(price) - .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build(), banner, null)))); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), - bidRequestBuilder -> bidRequestBuilder.app(App.builder() - .publisher(Publisher.builder().id("1001").build()).build())); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()).hasSize(1) - .flatExtracting(SeatBid::getBid) - .extracting(bid -> toExtBidPrebid(bid.getExt()).getEvents()) - .containsNull(); - } - - @Test - public void shouldIncrementCommonMetrics() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(TEN).build()))))); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someAlias", 1)), - builder -> builder - .site(Site.builder().publisher(Publisher.builder().id("accountId").build()).build()) - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("someAlias", "someBidder")) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(metrics).updateRequestBidderCardinalityMetric(1); - verify(metrics).updateAccountRequestMetrics(any(), eq(MetricName.openrtb2web)); - verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics( - eq("someBidder"), eq(MetricName.openrtb2web), eq(true)); - verify(metrics).updateAdapterResponseTime(eq("someBidder"), any(), anyInt()); - verify(metrics).updateAdapterRequestGotbidsMetrics(eq("someBidder"), any()); - verify(metrics).updateAdapterBidMetrics( - eq("someBidder"), any(), eq(10000L), eq(false), eq("banner")); - } - - @Test - public void shouldCallUpdateCookieMetricsWithExpectedValue() { - // given - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), - builder -> builder.app(App.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics( - eq("someBidder"), eq(MetricName.openrtb2web), eq(false)); - } - - @Test - public void shouldUseEmptyStringIfPublisherIdIsEmpty() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(TEN).build()))))); - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); - final Account account = Account.builder().id("").build(); - - // when - target.holdAuction(givenRequestContext(bidRequest, account)); - - // then - verify(metrics).updateAccountRequestMetrics(eq(account), eq(MetricName.openrtb2web)); - } - - @Test - public void shouldIncrementNoBidRequestsMetric() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(givenSeatBid(emptyList()))); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(metrics).updateAdapterRequestNobidMetrics(eq("someBidder"), any()); - } - - @Test - public void shouldIncrementGotBidsAndErrorMetricsIfBidderReturnsBidAndDifferentErrors() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(BidderSeatBid.builder() - .bids(singletonList(givenBidderBid(Bid.builder().impid("impId").price(TEN).build()))) - .errors(asList( - // two identical errors to verify corresponding metric is submitted only once - BidderError.badInput("rubicon error"), - BidderError.badInput("rubicon error"), - BidderError.badServerResponse("rubicon error"), - BidderError.failedToRequestBids("rubicon failed to request bids"), - BidderError.timeout("timeout error"), - BidderError.generic("timeout error"))) - .build())); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(metrics).updateAdapterRequestGotbidsMetrics(eq("someBidder"), any()); - verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.badinput)); - verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.badserverresponse)); - verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.failedtorequestbids)); - verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.timeout)); - verify(metrics).updateAdapterRequestErrorMetric(eq("someBidder"), eq(MetricName.unknown_error)); - } - - @Test - public void shouldPassResponseToPostProcessor() { - // given - final BidRequest bidRequest = givenBidRequest(emptyList()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(bidResponsePostProcessor).postProcess( - any(), - same(uidsCookie), - same(bidRequest), - any(), - eq(Account.builder() - .id("accountId") - .auction(AccountAuctionConfig.builder() - .events(AccountEventsConfig.of(true)) - .build()) - .build())); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(2.468)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(4.936)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(1).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - BidderBid.of(Bid.builder().impid("1234").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build()), - BidderBid.builder().type(xNative).bid(givenBid(identity())).build(), - BidderBid.builder().type(audio).bid(givenBid(identity())).build()))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - } - - @Test - public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { - // given - final Bidder bidder = mock(Bidder.class); - - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .auctiontimestamp(1000L) - .currency(ExtRequestCurrency.of(null, false)) - .bidadjustmentfactors(givenAdjustments) - .build()))); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.ONE); - } - - @Test - public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { - // given - final BidderBid bidToAccept = - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(ONE).build(), "USD"); - final BidderBid bidToReject = - givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(TEN).build(), "USD"); - - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(List.of(bidToAccept, bidToReject))); - - final BidRequest bidRequest = givenBidRequest(List.of( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), - identity()); - - given(priceFloorEnforcer.enforce(any(), any(), any(), any())) - .willReturn(AuctionParticipation.builder() - .bidder("bidder1") - .bidderResponse(BidderResponse.of( - "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) - .build()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsExactly(bidToAccept); - } - - @Test - public void shouldReturnBidsAcceptedByDsaEnforcer() { - // given - final BidderBid bidToAccept = - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(ONE).build(), "USD"); - final BidderBid bidToReject = - givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(TEN).build(), "USD"); - - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(List.of(bidToAccept, bidToReject))); - - final BidRequest bidRequest = givenBidRequest(List.of( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), - identity()); - - given(dsaEnforcer.enforce(any(), any(), any())) - .willReturn(AuctionParticipation.builder() - .bidder("bidder1") - .bidderResponse(BidderResponse.of( - "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) - .build()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsExactly(bidToAccept); - } - - @Test - public void shouldReturnBidResponseModifiedByAuctionResponseHooks() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(givenSeatBid(emptyList()))); - - doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( - false, - AuctionResponsePayloadImpl.of(BidResponse.builder().id("bidResponseId").build())))) - .when(hookStageExecutor).executeAuctionResponseStage(any(), any()); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2))); - - // when - final AuctionContext auctionContext = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(auctionContext.getBidResponse()) - .isEqualTo(BidResponse.builder().id("bidResponseId").build()); - } + assertThat(auctionContext.getBidResponse()) + .isEqualTo(BidResponse.builder().id("bidResponseId").build()); + } @Test public void shouldReturnEmptyBidResponseWhenRequestIsRejected() { @@ -4341,124 +3565,6 @@ public void shouldReturnBidResponseWithWarningWhenAnalyticsTagsDisabledAndReques ExtBidderError.of(999, "analytics.options.enableclientdetails not enabled for account")); } - @Test - public void shouldIncrementHooksGlobalMetrics() { - // given - final AuctionContext auctionContext = AuctionContext.builder() - .hookExecutionContext(HookExecutionContext.of( - Endpoint.openrtb2_auction, - stageOutcomes(givenAppliedToImpl(identity())))) - .debugContext(DebugContext.empty()) - .requestRejected(true) - .build(); - - // when - target.holdAuction(auctionContext); - - // then - verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); - verify(metrics).updateHooksMetrics( - eq("module1"), - eq(Stage.entrypoint), - eq("hook1"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.update)); - verify(metrics).updateHooksMetrics( - eq("module1"), - eq(Stage.entrypoint), - eq("hook2"), - eq(ExecutionStatus.invocation_failure), - eq(6L), - isNull()); - verify(metrics).updateHooksMetrics( - eq("module1"), - eq(Stage.entrypoint), - eq("hook2"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.no_action)); - verify(metrics).updateHooksMetrics( - eq("module2"), - eq(Stage.entrypoint), - eq("hook1"), - eq(ExecutionStatus.timeout), - eq(6L), - isNull()); - verify(metrics).updateHooksMetrics( - eq("module3"), - eq(Stage.auction_response), - eq("hook1"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.update)); - verify(metrics).updateHooksMetrics( - eq("module3"), - eq(Stage.auction_response), - eq("hook2"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.no_action)); - verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any()); - verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any()); - } - - @Test - public void shouldIncrementHooksGlobalAndAccountMetrics() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(givenSeatBid(emptyList()))); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2))); - final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder() - .hookExecutionContext(HookExecutionContext.of( - Endpoint.openrtb2_auction, - stageOutcomes(givenAppliedToImpl(identity())))) - .debugContext(DebugContext.empty()) - .build(); - - // when - target.holdAuction(auctionContext); - - // then - verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); - verify(metrics, times(6)).updateAccountHooksMetrics(any(), any(), any(), any()); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module1"), - eq(ExecutionStatus.success), - eq(ExecutionAction.update)); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module1"), - eq(ExecutionStatus.invocation_failure), - isNull()); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module1"), - eq(ExecutionStatus.success), - eq(ExecutionAction.no_action)); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module2"), - eq(ExecutionStatus.timeout), - isNull()); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module3"), - eq(ExecutionStatus.success), - eq(ExecutionAction.update)); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module3"), - eq(ExecutionStatus.success), - eq(ExecutionAction.no_action)); - verify(metrics, times(3)).updateAccountModuleDurationMetric(any(), any(), any()); - verify(metrics).updateAccountModuleDurationMetric(any(), eq("module1"), eq(14L)); - verify(metrics).updateAccountModuleDurationMetric(any(), eq("module2"), eq(6L)); - verify(metrics).updateAccountModuleDurationMetric(any(), eq("module3"), eq(8L)); - } - @Test public void shouldProperPopulateImpExtPrebidEvenIfInExtImpPrebidContainNotCorrectField() { // given @@ -4566,9 +3672,6 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb givenBidder(givenSingleSeatBid(bidderBid)); - given(responseBidValidator.validate(any(), any(), any(), any())) - .willReturn(ValidationResult.success()); - // when target.holdAuction(auctionContext); @@ -4647,6 +3750,62 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() .isEqualTo(BidResponse.builder().id("uniqId").build()); } + @Test + public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() { + // given + final Imp imp = givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")); + final BidRequest bidRequest = givenBidRequest(singletonList(imp), + bidRequestBuilder -> bidRequestBuilder.cur(singletonList("USD"))); + final AuctionContext auctionContext = givenRequestContext(bidRequest); + + given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create( + true, + null, + false, + null, + null, + null, + null, + null, + null, + null, + 0, + singletonList("CAD"), + false, + false, + CompressionType.NONE, + Ortb.of(false))); + given(bidResponseCreator.create( + argThat(argument -> argument.getAuctionParticipations().getFirst() + .getBidderResponse() + .equals(BidderResponse.of( + "bidder1", + BidderSeatBid.builder() + .warnings(Collections.singletonList( + BidderError.generic( + "No match between the configured currencies and bidRequest.cur" + ))) + .build(), + 0))), + any(), + any())) + .willReturn(Future.succeededFuture(BidResponse.builder().id("uniqId").build())); + + // when + final Future result = target.holdAuction(auctionContext); + + // then + assertThat(result.result()) + .extracting(AuctionContext::getBidResponse) + .isEqualTo(BidResponse.builder().id("uniqId").build()); + assertThat(result.result()) + .extracting(AuctionContext::getBidRejectionTrackers) + .extracting(rejectionTrackers -> rejectionTrackers.get("bidder1")) + .extracting(BidRejectionTracker::getRejectedImps) + .isEqualTo(Map.of("impId1", BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY)); + + } + @Test public void shouldConvertBidRequestOpenRTBVersionToConfiguredByBidder() { // given @@ -4706,6 +3865,66 @@ public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() { assertThat(timeoutCaptor.getAllValues()).containsExactly(450L); } + @Test + public void shouldDropBidsWithInvalidPrice() { + // given + final Bidder bidder = mock(Bidder.class); + final List bids = List.of( + Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), + Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), + Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), + Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); + final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); + + givenBidder("bidder", bidder, seatBid); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + final AuctionContext givenContext = givenRequestContext(bidRequest).with(DebugContext.empty()); + + // when + final AuctionContext result = target.holdAuction(givenContext).result(); + + // then + assertThat(result.getBidResponse().getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1); + assertThat(givenContext.getDebugWarnings()).isEmpty(); + verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); + } + + @Test + public void shouldDropBidsWithInvalidPriceAndAddDebugWarningsWhenDebugEnabled() { + // given + final Bidder bidder = mock(Bidder.class); + final List bids = List.of( + Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), + Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), + Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), + Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); + final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); + + givenBidder("bidder", bidder, seatBid); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + final AuctionContext givenContext = givenRequestContext(bidRequest) + .with(DebugContext.of(true, false, null)); + + // when + final AuctionContext result = target.holdAuction(givenContext).result(); + + // then + assertThat(result.getBidResponse().getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1); + assertThat(givenContext.getDebugWarnings()) + .containsExactlyInAnyOrder( + "Dropped bid 'invalid_bid_1'. Does not contain a positive (or zero if there is a deal) 'price'", + "Dropped bid 'invalid_bid_2'. Does not contain a positive (or zero if there is a deal) 'price'", + "Dropped bid 'invalid_bid_3'. Does not contain a positive (or zero if there is a deal) 'price'" + ); + verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); + } + private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { target = new ExchangeService( 0, @@ -4713,7 +3932,7 @@ private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { storedResponseProcessor, privacyEnforcementService, fpdResolver, - supplyChainResolver, + impAdjuster, supplyChainResolver, debugResolver, mediaTypeProcessor, uidUpdater, @@ -4721,17 +3940,13 @@ private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { timeoutFactory, ortbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, priceFloorProcessor, - dsaEnforcer, - bidAdjustmentFactorResolver, + bidsAdjuster, metrics, clock, jacksonMapper, diff --git a/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java b/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java index 12fed4e7f19..be7348abb74 100644 --- a/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java +++ b/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java @@ -15,8 +15,8 @@ import org.prebid.server.auction.model.IpAddress.IP; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java b/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java new file mode 100644 index 00000000000..4d90cc637d7 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java @@ -0,0 +1,260 @@ +package org.prebid.server.auction; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class HooksMetricsServiceTest extends VertxTest { + + @Mock + private Metrics metrics; + + private HooksMetricsService target; + + @BeforeEach + public void before() { + target = new HooksMetricsService(metrics); + } + + @Test + public void shouldIncrementHooksGlobalMetrics() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_auction, + stageOutcomes(givenAppliedToImpl()))) + .debugContext(DebugContext.empty()) + .requestRejected(true) + .build(); + + // when + target.updateHooksMetrics(auctionContext); + + // then + verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); + verify(metrics).updateHooksMetrics( + eq("module1"), + eq(Stage.entrypoint), + eq("hook1"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.update)); + verify(metrics).updateHooksMetrics( + eq("module1"), + eq(Stage.entrypoint), + eq("hook2"), + eq(ExecutionStatus.invocation_failure), + eq(6L), + isNull()); + verify(metrics).updateHooksMetrics( + eq("module1"), + eq(Stage.entrypoint), + eq("hook2"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.no_action)); + verify(metrics).updateHooksMetrics( + eq("module2"), + eq(Stage.entrypoint), + eq("hook1"), + eq(ExecutionStatus.timeout), + eq(6L), + isNull()); + verify(metrics).updateHooksMetrics( + eq("module3"), + eq(Stage.auction_response), + eq("hook1"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.update)); + verify(metrics).updateHooksMetrics( + eq("module3"), + eq(Stage.auction_response), + eq("hook2"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.no_action)); + verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any()); + verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any()); + } + + @Test + public void shouldIncrementHooksGlobalAndAccountMetrics() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_auction, + stageOutcomes(givenAppliedToImpl()))) + .debugContext(DebugContext.empty()) + .requestRejected(true) + .account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build()) + .build(); + + // when + target.updateHooksMetrics(auctionContext); + + // then + verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); + verify(metrics, times(6)).updateAccountHooksMetrics(any(), any(), any(), any()); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module1"), + eq(ExecutionStatus.success), + eq(ExecutionAction.update)); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module1"), + eq(ExecutionStatus.invocation_failure), + isNull()); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module1"), + eq(ExecutionStatus.success), + eq(ExecutionAction.no_action)); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module2"), + eq(ExecutionStatus.timeout), + isNull()); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module3"), + eq(ExecutionStatus.success), + eq(ExecutionAction.update)); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module3"), + eq(ExecutionStatus.success), + eq(ExecutionAction.no_action)); + verify(metrics, times(3)).updateAccountModuleDurationMetric(any(), any(), any()); + verify(metrics).updateAccountModuleDurationMetric(any(), eq("module1"), eq(14L)); + verify(metrics).updateAccountModuleDurationMetric(any(), eq("module2"), eq(6L)); + verify(metrics).updateAccountModuleDurationMetric(any(), eq("module3"), eq(8L)); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes(AppliedToImpl appliedToImp) { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.entrypoint, singletonList(StageExecutionOutcome.of( + "http-request", + asList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("Message 1-1") + .action(ExecutionAction.update) + .errors(asList("error message 1-1 1", "error message 1-1 2")) + .warnings(asList("warning message 1-1 1", "warning message 1-1 2")) + .debugMessages(asList("debug message 1-1 1", "debug message 1-1 2")) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + appliedToImp)))))) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(6L) + .status(ExecutionStatus.invocation_failure) + .message("Message 1-2") + .errors(asList("error message 1-2 1", "error message 1-2 2")) + .warnings(asList("warning message 1-2 1", "warning message 1-2 2")) + .build())), + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("Message 1-2") + .action(ExecutionAction.no_action) + .errors(asList("error message 1-2 3", "error message 1-2 4")) + .warnings(asList("warning message 1-2 3", "warning message 1-2 4")) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module2", "hook1")) + .executionTime(6L) + .status(ExecutionStatus.timeout) + .message("Message 2-1") + .errors(asList("error message 2-1 1", "error message 2-1 2")) + .warnings(asList("warning message 2-1 1", "warning message 2-1 2")) + .build())))))); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module3", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("Message 3-1") + .action(ExecutionAction.update) + .errors(asList("error message 3-1 1", "error message 3-1 2")) + .warnings(asList("warning message 3-1 1", "warning message 3-1 2")) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module3", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); + } + +} diff --git a/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java b/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java new file mode 100644 index 00000000000..717ca62dedd --- /dev/null +++ b/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java @@ -0,0 +1,257 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.validation.ImpValidator; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class ImpAdjusterTest extends VertxTest { + + @Mock + private ImpValidator impValidator; + + @Mock + private BidderCatalog bidderCatalog; + + private ImpAdjuster target; + + private BidderAliases bidderAliases; + + @BeforeEach + public void setUp() { + target = new ImpAdjuster(jacksonMapper, new JsonMerger(jacksonMapper), impValidator); + bidderAliases = BidderAliases.of( + Map.of("someBidderAlias", "someBidder"), Collections.emptyMap(), bidderCatalog); + } + + @Test + public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpIsNull() { + // given + final Imp givenImp = Imp.builder().build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpIsAbsent() { + // given + final Imp givenImp = Imp.builder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode())) + .build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void adjustShouldRemoveExpImpFromOriginalImpWhenImpExtPrebidImpHasEmptyBidder() { + // given + final Imp givenImp = Imp.builder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("imp", mapper.createObjectNode().set("someBidder", mapper.createObjectNode())))) + .build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode())).build(); + + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImp() throws ValidationException { + // given + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidder", givenBidderImp); + + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId2").build())).build()) + .bidfloor(new BigDecimal("2.0")) + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + verify(impValidator).validateImp(expectedImp); + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImpCaseInsensitive() throws ValidationException { + // given + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidder", givenBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "SOMEbiDDer", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId2").build())).build()) + .bidfloor(new BigDecimal("2.0")) + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + verify(impValidator).validateImp(expectedImp); + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImpCaseAliasBidder() throws ValidationException { + // given + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidderAlias", givenBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "SOMEbiDDer", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId2").build())).build()) + .bidfloor(new BigDecimal("2.0")) + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + verify(impValidator).validateImp(expectedImp); + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldReturnImpWithoutExpImpWhenResultingImpValidationFailed() throws ValidationException { + // given + doThrow(new ValidationException("imp validation failed")).when(impValidator).validateImp(any()); + + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidder", givenBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).containsOnly( + "imp.ext.prebid.imp.someBidder can not be merged into original imp [id=impId], " + + "reason: imp validation failed"); + } + + @Test + public void resolveImpShouldReturnImpWithoutExpWhenMergingFailed() { + // given + final ObjectNode invalidBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .put("pmp", 3); + + final Imp givenImp = givenImp("someBidder", invalidBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).hasSize(1).first() + .satisfies(message -> assertThat(message).startsWith( + "imp.ext.prebid.imp.someBidder can not be merged into original imp [id=impId]," + + " reason: Cannot construct instance of `com.iab.openrtb.request.Pmp`")); + } + + private static Imp givenImp(String bidder, ObjectNode bidderImpNode) { + final JsonNode givenExtPrebid = mapper.createObjectNode() + .put("prebidOriginAttr", "prebidOriginValue") + .set("imp", mapper.createObjectNode().set(bidder, bidderImpNode)); + + return Imp.builder() + .id("impId") + .tagid("impTagId") + .bidfloor(new BigDecimal("1.0")) + .bidfloorcur("USD") + .secure(1) + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId").build())).build()) + .iframebuster(Collections.singletonList("iframebuster")) + .ext(mapper.createObjectNode().put("originAttr", "originValue").set("prebid", givenExtPrebid)) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java index 8c3fd9ee24b..338af30f830 100644 --- a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java +++ b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java @@ -26,6 +26,19 @@ public void createFromStringShouldThrowPrebidExceptionIfInvalidStringType() { assertThatExceptionOfType(PreBidException.class).isThrownBy(() -> PriceGranularity.createFromString("invalid")); } + @Test + public void createFromStringOrDefaultShouldCreateMedPriceGranularityWhenInvalidStringType() { + // given and when + final PriceGranularity defaultPriceGranularity = PriceGranularity.createFromStringOrDefault( + "invalid"); + + // then + assertThat(defaultPriceGranularity.getRangesMax()).isEqualByComparingTo(BigDecimal.valueOf(20)); + assertThat(defaultPriceGranularity.getPrecision()).isEqualTo(2); + assertThat(defaultPriceGranularity.getRanges()).containsOnly( + ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))); + } + @Test public void createCustomPriceGranularityByStringLow() { // given and when diff --git a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java index b284cf60f3a..3976a4bbd8f 100644 --- a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java @@ -13,7 +13,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; @@ -126,7 +126,7 @@ public void skipAuctionShouldReturnFailedFutureWhenStoredResponseSeatBidAndIdAre final AuctionContext auctionContext = AuctionContext.builder() .bidRequest(BidRequest.builder() .ext(ExtRequest.of(ExtRequestPrebid.builder() - .storedAuctionResponse(ExtStoredAuctionResponse.of(null, null)) + .storedAuctionResponse(ExtStoredAuctionResponse.of(null, null, null)) .build())) .build()) .build(); @@ -147,7 +147,7 @@ public void skipAuctionShouldReturnFailedFutureWhenStoredResponseSeatBidAndIdAre public void skipAuctionShouldReturnBidResponseWithSeatBidsFromStoredAuctionResponse() { // given final List givenSeatBids = givenSeatBids("bidId1", "bidId2"); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -179,7 +179,8 @@ public void skipAuctionShouldReturnBidResponseWithSeatBidsFromStoredAuctionRespo @Test public void skipAuctionShouldReturnEmptySeatBidsWhenSeatBidIsNull() { // given - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", singletonList(null)); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of( + "id", singletonList(null), null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -214,7 +215,7 @@ public void skipAuctionShouldReturnEmptySeatBidsWhenSeatBidIsNull() { public void skipAuctionShouldReturnEmptySeatBidsWhenSeatIsEmpty() { // given final List givenSeatBids = singletonList(SeatBid.builder().seat("").build()); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -249,7 +250,7 @@ public void skipAuctionShouldReturnEmptySeatBidsWhenSeatIsEmpty() { public void skipAuctionShouldReturnEmptySeatBidsWhenBidsAreEmpty() { // given final List givenSeatBids = singletonList(SeatBid.builder().seat("seat").bid(emptyList()).build()); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -283,7 +284,7 @@ public void skipAuctionShouldReturnEmptySeatBidsWhenBidsAreEmpty() { @Test public void skipAuctionShouldReturnBidResponseWithEmptySeatBidsWhenNoValueAvailableById() { // given - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .timeoutContext(TimeoutContext.of(1000L, timeout, 0)) @@ -320,7 +321,7 @@ public void skipAuctionShouldReturnBidResponseWithEmptySeatBidsWhenNoValueAvaila public void skipAuctionShouldReturnBidResponseWithStoredSeatBidsByProvidedId() { // given final List givenSeatBids = givenSeatBids("bidId1", "bidId2"); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .timeoutContext(TimeoutContext.of(1000L, timeout, 0)) diff --git a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java index 8cbd5bc8b70..9b9aa5aba48 100644 --- a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java @@ -19,7 +19,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java index 7b88fb26c34..a137578ef14 100644 --- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java @@ -23,8 +23,8 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; @@ -83,7 +83,7 @@ public void setUp() { @Test public void getStoredResponseResultShouldReturnSeatBidsForAuctionResponseId() throws JsonProcessingException { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("1", @@ -149,7 +149,7 @@ public void getStoredResponseResultShouldAddImpToRequiredRequestWhenItsStoredAuc @Test public void getStoredResponseResultShouldReturnFailedFutureWhenErrorHappenedDuringRetrievingStoredResponse() { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.failedFuture(new PreBidException("Failed."))); @@ -194,7 +194,7 @@ public void getStoredResponseResultShouldReturnResultForBidAndAuctionStoredRespo // given final Imp imp1 = givenImp( "impId1", - ExtStoredAuctionResponse.of("storedAuctionResponseId", Collections.emptyList()), + ExtStoredAuctionResponse.of("storedAuctionResponseId", Collections.emptyList(), null), null); final Imp imp2 = givenImp( "impId2", @@ -228,7 +228,7 @@ public void getStoredResponseResultShouldReturnResultForBidAndAuctionStoredRespo @Test public void getStoredResponseResultShouldThrowInvalidRequestExceptionWhenStoredAuctionResponseWasNotFound() { // given - final Imp imp1 = givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponseId", null), null); + final Imp imp1 = givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponseId", null, null), null); given(applicationSettings.getStoredResponses(any(), any())).willReturn( Future.succeededFuture(StoredResponseDataResult.of(emptyMap(), emptyList()))); @@ -247,8 +247,8 @@ public void getStoredResponseResultShouldThrowInvalidRequestExceptionWhenStoredA public void getStoredResponseResultShouldMergeStoredSeatBidsForTheSameBidder() throws JsonProcessingException { // given final List imps = asList( - givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponse1", null), null), - givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2", null), null)); + givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponse1", null, null), null), + givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2", null, null), null)); final Map storedResponse = new HashMap<>(); storedResponse.put("storedAuctionResponse1", mapper.writeValueAsString(asList( @@ -275,9 +275,65 @@ public void getStoredResponseResultShouldMergeStoredSeatBidsForTheSameBidder() t SeatBid.builder() .seat("rubicon") .bid(asList( - Bid.builder().id("id2").impid("impId2").build(), - Bid.builder().id("id3").impid("impId1").build() - )) + Bid.builder().id("id3").impid("impId1").build(), + Bid.builder().id("id2").impid("impId2").build())) + .build()), + emptyMap())); + } + + @Test + public void getStoredResponseResultShouldUseStoredSeatBidsFromRequest() throws JsonProcessingException { + // given + final List imps = asList( + givenImp( + "impId1", + ExtStoredAuctionResponse.of( + "storedAuctionResponse1", + null, + SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id4").build())) + .build()), + null), + givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2", null, null), null), + givenImp( + "impId3", + ExtStoredAuctionResponse.of( + null, + null, + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id5").build())) + .build()), + null)); + + final Map storedResponse = new HashMap<>(); + storedResponse.put("storedAuctionResponse1", mapper.writeValueAsString(asList( + SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())).build(), + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id3").build())).build()))); + storedResponse.put("storedAuctionResponse2", mapper.writeValueAsString(singletonList( + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())).build()))); + + given(applicationSettings.getStoredResponses(any(), any())).willReturn( + Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList()))); + + // when + final Future result = + target.getStoredResponseResult(imps, timeout); + + // then + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + emptyList(), + asList( + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id5").impid("impId3").build())) + .build(), + SeatBid.builder() + .seat("rubicon") + .bid(asList( + Bid.builder().id("id4").impid("impId1").build(), + Bid.builder().id("id2").impid("impId2").build())) .build()), emptyMap())); } @@ -301,7 +357,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStor throws JsonProcessingException { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.succeededFuture(StoredResponseDataResult.of( @@ -328,7 +384,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto // given final List imps = singletonList( - givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.succeededFuture(StoredResponseDataResult.of( @@ -352,7 +408,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto @Test public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCannotBeParsed() { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())).willReturn(Future.succeededFuture( StoredResponseDataResult.of(singletonMap("1", "{invalid"), emptyList()))); diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index f63b6d32772..879bd7873c2 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -39,7 +39,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -70,7 +70,7 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -105,7 +105,7 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { true, false, true, - false, + null, 0, null, null, @@ -148,7 +148,7 @@ public void shouldIncludeFormatOpenrtb() { true, false, true, - false, + null, 0, null, null, @@ -174,7 +174,7 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -201,7 +201,7 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { true, false, false, - true, + "mobile-app", 0, null, null, @@ -229,7 +229,7 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { true, false, false, - false, + null, 0, null, null, @@ -255,7 +255,7 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { true, false, false, - false, + null, 0, null, null, @@ -281,7 +281,7 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() false, false, false, - false, + null, 0, null, null, @@ -307,7 +307,7 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { true, false, false, - false, + null, 0, null, null, @@ -333,7 +333,7 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() true, false, false, - false, + null, 20, null, null, @@ -360,7 +360,7 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar false, false, false, - false, + null, 7, null, null, @@ -387,7 +387,7 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { true, false, true, - true, + "mobile-app", 6, null, null, @@ -415,7 +415,7 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() true, false, false, - false, + null, 0, null, null, @@ -448,7 +448,7 @@ public void shouldTruncateKeysFromResolver() { true, false, false, - false, + null, 20, null, null, @@ -480,7 +480,7 @@ public void shouldIncludeKeywordsFromResolver() { true, false, false, - false, + null, 0, null, null, @@ -506,7 +506,7 @@ public void shouldIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsTrue() { false, true, false, - false, + null, 0, null, null, @@ -532,7 +532,7 @@ public void shouldNotIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsFalse() { false, false, false, - false, + null, 0, null, null, diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java index 3946162ff92..66442d47c93 100644 --- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java @@ -24,7 +24,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java index 8103416ef22..60ff60318e1 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java @@ -171,6 +171,7 @@ private static BidderInfo givenBidderInfo(List appMediaTypes, doohMediaType, emptyList(), 0, + null, false, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java index 1e89f446502..e2394769585 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java @@ -145,6 +145,40 @@ public void processShouldUseRequestLevelPreferredMediaTypeFirst() { assertThat(result.getErrors()).isEmpty(); } + @Test + public void processShouldUseRequestLevelPreferredMediaTypeFirstCaseInsensitive() { + // given + given(bidderAliases.resolveBidder(BIDDER)).willReturn("resolvedBidderName"); + given(bidderCatalog.bidderInfoByName("resolvedBidderName")).willReturn(givenBidderInfo(false)); + + final ObjectNode bidderControls = mapper.createObjectNode(); + bidderControls.putObject(BIDDER.toUpperCase()).put("prefmtype", "video"); + + final BidRequest bidRequest = givenBidRequest( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .biddercontrols(bidderControls) + .build())), + givenImp(BANNER, VIDEO, AUDIO, NATIVE)); + + final Account account = givenAccount(Map.of("resolvedBidderName", AUDIO)); + + // when + final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + + // then + assertThat(result.isRejected()).isFalse(); + assertThat(result.getBidRequest()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .containsExactly(givenImp(VIDEO)); + assertThat(result.getBidRequest()) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBiddercontrols) + .isNull(); + assertThat(result.getErrors()).isEmpty(); + } + @Test public void processShouldSkipImpsWithSingleMediaType() { // given @@ -241,6 +275,7 @@ private static BidderInfo givenBidderInfo(boolean multiFormatSupported) { emptyList(), emptyList(), 0, + emptyList(), false, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java b/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java index 49185140d2b..502b09108b5 100644 --- a/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java +++ b/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java @@ -1,14 +1,19 @@ package org.prebid.server.auction.model; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.prebid.server.bidder.model.BidderBid; +import java.util.List; import java.util.Map; import java.util.Set; import static java.util.Collections.singleton; -import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; public class BidRejectionTrackerTest { @@ -16,90 +21,214 @@ public class BidRejectionTrackerTest { @BeforeEach public void setUp() { - target = new BidRejectionTracker("bidder", singleton("1"), 0); + target = new BidRejectionTracker("bidder", singleton("impId1"), 0); } @Test - public void succeedShouldRestoreBidderFromRejection() { + public void succeedShouldRestoreImpFromImpRejection() { // given - target.reject("1", BidRejectionReason.OTHER_ERROR); + target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL); // when - target.succeed("1"); + final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + target.succeed(singleton(bid)); // then - assertThat(target.getRejectionReasons()).isEmpty(); + assertThat(target.getRejectedImps()).isEmpty(); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(null, BidRejectionReason.ERROR_GENERAL)))); } @Test - public void succeedShouldIgnoreUninvolvedImpIds() { + public void succeedShouldRestoreImpFromBidRejection() { // given - target.reject("1", BidRejectionReason.OTHER_ERROR); + final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + target.rejectBid(bid, BidRejectionReason.ERROR_GENERAL); // when - target.succeed("2"); + target.succeed(singleton(bid)); // then - assertThat(target.getRejectionReasons()) - .isEqualTo(singletonMap("1", BidRejectionReason.OTHER_ERROR)); + assertThat(target.getRejectedImps()).isEmpty(); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(bid, BidRejectionReason.ERROR_GENERAL)))); } @Test - public void rejectShouldRecordRejectionFirstTimeIfImpIdIsInvolved() { + public void succeedShouldIgnoreUninvolvedImpIdsOnImpRejection() { + // given + target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL); + + // when + final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId2").build()).build(); + target.succeed(singleton(bid)); + + // then + assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL)); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(null, BidRejectionReason.ERROR_GENERAL)))); + } + + @Test + public void succeedShouldIgnoreUninvolvedImpIdsOnBidRejection() { + // given + final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + target.rejectBid(bid1, BidRejectionReason.ERROR_GENERAL); + + // when + final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId2").build()).build(); + target.succeed(singleton(bid2)); + + // then + assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL)); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(bid1, BidRejectionReason.ERROR_GENERAL)))); + } + + @Test + public void rejectImpShouldRecordImpRejectionFirstTimeIfImpIdIsInvolved() { + // when + target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL); + + // then + assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL)); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(null, BidRejectionReason.ERROR_GENERAL)))); + } + + @Test + public void rejectBidShouldRecordBidRejectionFirstTimeIfImpIdIsInvolved() { // when - target.reject("1", BidRejectionReason.OTHER_ERROR); + final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + target.rejectBid(bid, BidRejectionReason.ERROR_GENERAL); // then - assertThat(target.getRejectionReasons()) - .isEqualTo(singletonMap("1", BidRejectionReason.OTHER_ERROR)); + assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL)); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(bid, BidRejectionReason.ERROR_GENERAL)))); } @Test - public void rejectShouldNotRecordRejectionIfImpIdIsNotInvolved() { + public void rejectBidShouldRecordBidRejectionAfterPreviouslySucceededBid() { + // given + final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId1").build()).build(); + target.succeed(Set.of(bid1, bid2)); + + // when + target.rejectBid(bid1, BidRejectionReason.ERROR_GENERAL); + + // then + assertThat(target.getRejectedImps()).isEmpty(); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of(Pair.of(bid1, BidRejectionReason.ERROR_GENERAL)))); + } + + @Test + public void rejectImpShouldNotRecordImpRejectionIfImpIdIsAlreadyRejected() { + // given + target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL); + + // when + target.rejectImp("impId1", BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + + // then + assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL)); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of( + Pair.of(null, BidRejectionReason.ERROR_GENERAL), + Pair.of(null, BidRejectionReason.ERROR_INVALID_BID_RESPONSE)))); + } + + @Test + public void rejectBidShouldNotRecordImpRejectionButRecordBidRejectionEvenIfImpIsAlreadyRejected() { + // given + final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + target.rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_GENERAL); + // when - target.reject("2", BidRejectionReason.OTHER_ERROR); + final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId1").build()).build(); + target.rejectBid(bid2, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); // then - assertThat(target.getRejectionReasons()).doesNotContainKey("2"); + assertThat(target.getRejectedImps()) + .containsOnly(entry("impId1", BidRejectionReason.RESPONSE_REJECTED_GENERAL)); + assertThat(target.getRejectedBids()) + .containsOnly(entry("impId1", List.of( + Pair.of(bid1, BidRejectionReason.RESPONSE_REJECTED_GENERAL), + Pair.of(bid2, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR)))); } @Test - public void rejectShouldNotRecordRejectionIfImpIdIsAlreadyRejected() { + public void rejectAllImpsShouldTryRejectingEachImpId() { // given - target.reject("1", BidRejectionReason.OTHER_ERROR); + target = new BidRejectionTracker("bidder", Set.of("impId1", "impId2", "impId3"), 0); + target.rejectImp("impId1", BidRejectionReason.NO_BID); // when - target.reject("1", BidRejectionReason.FAILED_TO_REQUEST_BIDS); + target.rejectAllImps(BidRejectionReason.ERROR_TIMED_OUT); // then - assertThat(target.getRejectionReasons()) - .isEqualTo(singletonMap("1", BidRejectionReason.OTHER_ERROR)); + assertThat(target.getRejectedImps()) + .isEqualTo(Map.of( + "impId1", BidRejectionReason.NO_BID, + "impId2", BidRejectionReason.ERROR_TIMED_OUT, + "impId3", BidRejectionReason.ERROR_TIMED_OUT)); + + assertThat(target.getRejectedBids()) + .containsOnly( + entry("impId1", List.of( + Pair.of(null, BidRejectionReason.NO_BID), + Pair.of(null, BidRejectionReason.ERROR_TIMED_OUT))), + entry("impId2", List.of(Pair.of(null, BidRejectionReason.ERROR_TIMED_OUT))), + entry("impId3", List.of(Pair.of(null, BidRejectionReason.ERROR_TIMED_OUT)))); } @Test - public void rejectAllShouldTryRejectingEachImpId() { + public void rejectBidsShouldTryRejectingEachBid() { // given - target = new BidRejectionTracker("bidder", Set.of("1", "2", "3"), 0); - target.reject("1", BidRejectionReason.NO_BID); + target = new BidRejectionTracker("bidder", Set.of("impId1", "impId2", "impId3"), 0); + final BidderBid bid0 = BidderBid.builder().bid(Bid.builder().id("bidId0").impid("impId1").build()).build(); + target.rejectBid(bid0, BidRejectionReason.RESPONSE_REJECTED_GENERAL); // when - target.rejectAll(BidRejectionReason.TIMED_OUT); + final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build(); + final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId2").build()).build(); + final BidderBid bid3 = BidderBid.builder().bid(Bid.builder().id("bidId3").impid("impId3").build()).build(); + target.rejectBids(Set.of(bid1, bid2, bid3), BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); // then - assertThat(target.getRejectionReasons()) + assertThat(target.getRejectedImps()) .isEqualTo(Map.of( - "1", BidRejectionReason.NO_BID, - "2", BidRejectionReason.TIMED_OUT, - "3", BidRejectionReason.TIMED_OUT)); + "impId1", BidRejectionReason.RESPONSE_REJECTED_GENERAL, + "impId2", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY, + "impId3", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY)); + + assertThat(target.getRejectedBids()) + .containsOnly( + entry("impId1", List.of( + Pair.of(bid0, BidRejectionReason.RESPONSE_REJECTED_GENERAL), + Pair.of(bid1, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY))), + entry("impId2", List.of(Pair.of(bid2, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY))), + entry("impId3", List.of(Pair.of(bid3, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY)))); } @Test - public void getRejectionReasonsShouldTreatUnsuccessfulBidsAsNoBidRejection() { + public void getRejectedImpsShouldTreatUnsuccessfulImpsAsNoBidRejection() { // given - target = new BidRejectionTracker("bidder", Set.of("1", "2"), 0); - target.succeed("2"); + target = new BidRejectionTracker("bidder", Set.of("impId1", "impId2"), 0); + final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId2").build()).build(); + target.succeed(singleton(bid)); // then - assertThat(target.getRejectionReasons()).isEqualTo(singletonMap("1", BidRejectionReason.NO_BID)); + assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.NO_BID)); + } + + @Test + public void rejectImpShouldFailRejectingWithReasonThatImpliesExistingBidToReject() { + assertThatThrownBy(() -> target.rejectImp("impId1", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The non-bid code 300 and higher assumes " + + "that there is a rejected bid that shouldn't be lost"); } } diff --git a/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java b/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java index fd005459913..477e67ffd0b 100644 --- a/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java @@ -14,7 +14,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java b/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java index 453210aabd1..1cf5961ff4e 100644 --- a/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java @@ -14,7 +14,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java index a6c2e3f3ae9..6e4662a501d 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java @@ -9,6 +9,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; @@ -32,6 +33,9 @@ public class ActivityEnforcementTest { @Mock private ActivityInfrastructure activityInfrastructure; + @Mock + private BidderAliases bidderAliases; + @BeforeEach public void setUp() { target = new ActivityEnforcement(userFpdActivityMask); @@ -58,7 +62,8 @@ public void enforceShouldReturnExpectedResult() { final AuctionContext context = givenAuctionContext(); // when - final List result = target.enforce(singletonList(bidderPrivacyResult), context).result(); + final List result = + target.enforce(context, bidderAliases, singletonList(bidderPrivacyResult)).result(); //then assertThat(result).allSatisfy(privacyResult -> { diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java index b8287e6c086..9bf76d427f9 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java @@ -30,7 +30,6 @@ import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.UnaryOperator; @@ -76,6 +75,7 @@ public void setUp() { null, null, 0, + null, true, false, null, @@ -88,16 +88,17 @@ public void setUp() { } @Test - public void enforceShouldReturnEmptyListWhenCcpaNotEnforced() { + public void enforceShouldNotModifyListWhenCcpaIsNotEnforced() { // given final AuctionContext auctionContext = givenAuctionContext(context -> context .privacyContext(PrivacyContext.of(Privacy.builder().ccpa(Ccpa.of("1YN-")).build(), null, null))); + final List initialResults = givenPrivacyResults(givenUser(), givenDevice()); // when - final List result = target.enforce(auctionContext, null, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults); verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -111,13 +112,15 @@ public void enforceShouldConsiderEnforceCcpaConfigurationProperty() { // given final AuctionContext auctionContext = givenAuctionContext(context -> context.account(Account.empty("id"))); + final List initialResults = givenPrivacyResults(givenUser(), givenDevice()); + target = new CcpaEnforcement(userFpdCcpaMask, bidderCatalog, metrics, false); // when - final List result = target.enforce(auctionContext, null, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults); verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -135,12 +138,13 @@ public void enforceShouldConsiderAccountCcpaEnabledProperty() { .ccpa(AccountCcpaConfig.builder().enabled(false).build()) .build()) .build())); + final List initialResults = givenPrivacyResults(givenUser(), givenDevice()); // when - final List result = target.enforce(auctionContext, null, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults); verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -154,12 +158,13 @@ public void enforceShouldConsiderAccountCcpaEnabledForRequestTypeProperty() { // given final AuctionContext auctionContext = givenAuctionContext(context -> context .requestTypeMetric(MetricName.openrtb2app)); + final List initialResults = givenPrivacyResults(givenUser(), givenDevice()); // when - final List result = target.enforce(auctionContext, null, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults); verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -173,21 +178,21 @@ public void enforceShouldTreatAllBiddersAsNoSale() { // given final AuctionContext auctionContext = givenAuctionContext(context -> context .bidRequest(BidRequest.builder() - .device(Device.builder().ip("originalDevice").build()) + .device(givenDevice()) .ext(ExtRequest.of(ExtRequestPrebid.builder() .nosale(singletonList("*")) .build())) .build())); - final Map bidderToUser = Map.of( - "bidder", User.builder().id("originalUser").build(), - "noSale", User.builder().id("originalUser").build()); + final List initialResults = List.of( + BidderPrivacyResult.builder().requestBidder("bidder").user(givenUser()).device(givenDevice()).build(), + BidderPrivacyResult.builder().requestBidder("noSale").user(givenUser()).device(givenDevice()).build()); // when - final List result = target.enforce(auctionContext, bidderToUser, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults); verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -213,6 +218,7 @@ public void enforceShouldSkipNoSaleBiddersAndNotEnforcedByBidderConfig() { null, null, 0, + null, false, false, null, @@ -220,15 +226,23 @@ public void enforceShouldSkipNoSaleBiddersAndNotEnforcedByBidderConfig() { final AuctionContext auctionContext = givenAuctionContext(identity()); - final Map bidderToUser = Map.of( - "bidderAlias", User.builder().id("originalUser").build(), - "noSale", User.builder().id("originalUser").build()); + final List initialResults = List.of( + BidderPrivacyResult.builder() + .requestBidder("bidderAlias") + .user(givenUser()) + .device(givenDevice()) + .build(), + BidderPrivacyResult.builder() + .requestBidder("noSale") + .user(givenUser()) + .device(givenDevice()) + .build()); // when - final List result = target.enforce(auctionContext, bidderToUser, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults); verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -248,20 +262,18 @@ public void enforceShouldReturnExpectedResult() { final AuctionContext auctionContext = givenAuctionContext(identity()); - final Map bidderToUser = Map.of( - "bidder", User.builder().id("originalUser").build(), - "noSale", User.builder().id("originalUser").build()); + final List initialResults = List.of( + BidderPrivacyResult.builder().requestBidder("bidder").user(givenUser()).device(givenDevice()).build(), + BidderPrivacyResult.builder().requestBidder("noSale").user(givenUser()).device(givenDevice()).build()); // when - final List result = target.enforce(auctionContext, bidderToUser, aliases).result(); + final List result = target.enforce(auctionContext, aliases, initialResults).result(); // then - assertThat(result) - .hasSize(1) - .allSatisfy(privacyResult -> { - assertThat(privacyResult.getUser()).isSameAs(maskedUser); - assertThat(privacyResult.getDevice()).isSameAs(maskedDevice); - }); + assertThat(result).containsExactlyInAnyOrder( + BidderPrivacyResult.builder().requestBidder("bidder").user(maskedUser).device(maskedDevice).build(), + BidderPrivacyResult.builder().requestBidder("noSale").user(givenUser()).device(givenDevice()).build()); + verify(metrics).updatePrivacyCcpaMetrics( eq(activityInfrastructure), eq(true), @@ -276,7 +288,7 @@ private AuctionContext givenAuctionContext( final AuctionContext.AuctionContextBuilder initialContext = AuctionContext.builder() .activityInfrastructure(activityInfrastructure) .bidRequest(BidRequest.builder() - .device(Device.builder().ip("originalDevice").build()) + .device(givenDevice()) .ext(ExtRequest.of(ExtRequestPrebid.builder() .nosale(singletonList("noSale")) .build())) @@ -299,4 +311,16 @@ private AuctionContext givenAuctionContext( return auctionContextCustomizer.apply(initialContext).build(); } + + private static List givenPrivacyResults(User user, Device device) { + return singletonList(BidderPrivacyResult.builder().requestBidder("bidder").user(user).device(device).build()); + } + + private static User givenUser() { + return User.builder().id("originalUser").build(); + } + + private static Device givenDevice() { + return Device.builder().ip("originalDevice").build(); + } } diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java index 4b7e7de8628..9e9d47d0488 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java @@ -9,6 +9,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask; @@ -17,13 +18,14 @@ import org.prebid.server.privacy.model.PrivacyContext; import java.util.List; -import java.util.Map; import java.util.Set; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; @ExtendWith(MockitoExtension.class) public class CoppaEnforcementTest { @@ -34,6 +36,8 @@ public class CoppaEnforcementTest { private Metrics metrics; @Mock private ActivityInfrastructure activityInfrastructure; + @Mock + private BidderAliases bidderAliases; private CoppaEnforcement target; @@ -43,29 +47,33 @@ public void setUp() { } @Test - public void isApplicableShouldReturnFalse() { + public void enforceShouldNotMaskDataWhenNotApplicable() { // given final AuctionContext auctionContext = AuctionContext.builder() + .activityInfrastructure(activityInfrastructure) .privacyContext(PrivacyContext.of(Privacy.builder().coppa(0).build(), null, null)) + .bidRequest(BidRequest.builder().build()) .build(); - // when and then - assertThat(target.isApplicable(auctionContext)).isFalse(); - } + final List initialResults = singletonList( + BidderPrivacyResult.builder() + .requestBidder("bidder") + .user(User.builder().id("originalUser").build()) + .device(Device.builder().ip("originalDevice").build()) + .build()); - @Test - public void isApplicableShouldReturnTrue() { - // given - final AuctionContext auctionContext = AuctionContext.builder() - .privacyContext(PrivacyContext.of(Privacy.builder().coppa(1).build(), null, null)) - .build(); + // when + final List results = + target.enforce(auctionContext, bidderAliases, initialResults).result(); - // when and then - assertThat(target.isApplicable(auctionContext)).isTrue(); + // then + assertThat(results).containsExactlyInAnyOrderElementsOf(initialResults); + verifyNoInteractions(userFpdCoppaMask); + verifyNoInteractions(metrics); } @Test - public void enforceShouldReturnExpectedResultAndEmitMetrics() { + public void enforceShouldMaskDataAndEmitMetricsWhenApplicable() { // given final User maskedUser = User.builder().id("maskedUser").build(); final Device maskedDevice = Device.builder().ip("maskedDevice").build(); @@ -75,12 +83,19 @@ public void enforceShouldReturnExpectedResultAndEmitMetrics() { final AuctionContext auctionContext = AuctionContext.builder() .activityInfrastructure(activityInfrastructure) - .bidRequest(BidRequest.builder().device(Device.builder().ip("originalDevice").build()).build()) + .privacyContext(PrivacyContext.of(Privacy.builder().coppa(1).build(), null, null)) + .bidRequest(BidRequest.builder().build()) .build(); - final Map bidderToUser = Map.of("bidder", User.builder().id("originalUser").build()); + + final List initialResults = singletonList( + BidderPrivacyResult.builder() + .requestBidder("bidder") + .user(User.builder().id("originalUser").build()) + .device(Device.builder().ip("originalDevice").build()) + .build()); // when - final List result = target.enforce(auctionContext, bidderToUser).result(); + final List result = target.enforce(auctionContext, bidderAliases, initialResults).result(); // then assertThat(result).allSatisfy(privacyResult -> { diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java index f1a49f91738..4333eebb5d8 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java @@ -1,98 +1,70 @@ package org.prebid.server.auction.privacy.enforcement; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; -import org.apache.commons.collections4.ListUtils; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import java.util.List; import java.util.Map; -import static java.util.Collections.singleton; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; +import static java.util.Collections.singletonMap; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.assertion.FutureAssertion.assertThat; @ExtendWith(MockitoExtension.class) public class PrivacyEnforcementServiceTest { @Mock - private CoppaEnforcement coppaEnforcement; - @Mock - private CcpaEnforcement ccpaEnforcement; - @Mock - private TcfEnforcement tcfEnforcement; - @Mock - private ActivityEnforcement activityEnforcement; - - private PrivacyEnforcementService target; + private PrivacyEnforcement firstEnforcement; - @BeforeEach - public void setUp() { - target = new PrivacyEnforcementService( - coppaEnforcement, - ccpaEnforcement, - tcfEnforcement, - activityEnforcement); - } - - @Test - public void maskShouldUseCoppaEnforcementIfApplicable() { - // given - given(coppaEnforcement.isApplicable(any())).willReturn(true); - - final List bidderPrivacyResults = singletonList(null); - given(coppaEnforcement.enforce(any(), any())).willReturn(Future.succeededFuture(bidderPrivacyResults)); - - // when - final List result = target.mask(null, null, null).result(); + @Mock + private PrivacyEnforcement secondEnforcement; - // then - assertThat(result).isSameAs(bidderPrivacyResults); - verifyNoInteractions(ccpaEnforcement); - verifyNoInteractions(tcfEnforcement); - verifyNoInteractions(activityEnforcement); - } + @Mock + private BidderAliases bidderAliases; @Test - public void maskShouldReturnExpectedResult() { + public void maskShouldPassBidderPrivacyThroughAllEnforcements() { // given - given(coppaEnforcement.isApplicable(any())).willReturn(false); + final BidderPrivacyResult expectedResult = BidderPrivacyResult.builder() + .requestBidder("bidder") + .user(User.EMPTY) + .device(Device.builder().build()) + .build(); - given(ccpaEnforcement.enforce(any(), any(), any())).willReturn(Future.succeededFuture( - singletonList(BidderPrivacyResult.builder().requestBidder("bidder1").build()))); + given(firstEnforcement.enforce(any(), any(), any())) + .willReturn(Future.succeededFuture(singletonList(expectedResult))); + given(secondEnforcement.enforce(any(), any(), any())) + .willReturn(Future.succeededFuture(singletonList(expectedResult))); - given(tcfEnforcement.enforce(any(), any(), eq(singleton("bidder0")), any())) - .willReturn(Future.succeededFuture( - singletonList(BidderPrivacyResult.builder().requestBidder("bidder0").build()))); + final PrivacyEnforcementService target = new PrivacyEnforcementService( + List.of(firstEnforcement, secondEnforcement)); - given(activityEnforcement.enforce(any(), any())) - .willAnswer(invocation -> Future.succeededFuture(ListUtils.union( - invocation.getArgument(0), - singletonList(BidderPrivacyResult.builder().requestBidder("bidder2").build())))); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(BidRequest.builder().device(Device.builder().build()).build()) + .build(); - final Map bidderToUser = Map.of( - "bidder0", User.builder().build(), - "bidder1", User.builder().build()); + final User user = User.builder().id("originalUser").build(); + final Map bidderToUser = singletonMap("bidder", user); // when - final List result = target.mask(null, bidderToUser, null).result(); + final Future> result = target.mask(auctionContext, bidderToUser, bidderAliases); // then - assertThat(result).containsExactly( - BidderPrivacyResult.builder().requestBidder("bidder1").build(), - BidderPrivacyResult.builder().requestBidder("bidder0").build(), - BidderPrivacyResult.builder().requestBidder("bidder2").build()); - verify(coppaEnforcement, times(0)).enforce(any(), any()); + assertThat(result) + .isSucceeded() + .unwrap() + .asList() + .containsExactlyInAnyOrder(expectedResult); } } diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java index e8222accf25..0ed33b4cce3 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java @@ -119,20 +119,20 @@ public void enforceShouldEmitExpectedMetricsWhenUserAndDeviceHavePrivacyData() { "bidder6", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo), "bidder7", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd))); - final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData()); - final Map bidderToUser = Map.of( - "bidder0", givenUserWithPrivacyData(), - "bidder1Alias", givenUserWithPrivacyData(), - "bidder2", givenUserWithPrivacyData(), - "bidder3", givenUserWithPrivacyData(), - "bidder4", givenUserWithPrivacyData(), - "bidder5", givenUserWithPrivacyData(), - "bidder6", givenUserWithPrivacyData(), - "bidder7", givenUserWithPrivacyData()); - final Set bidders = Set.of(); + final Device device = givenDeviceWithPrivacyData(); + final AuctionContext auctionContext = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder0", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder1Alias", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder2", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder3", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder4", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder5", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder6", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder7", givenUserWithPrivacyData(), device)); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder0", false, false, false, false, false, true); @@ -155,14 +155,13 @@ public void enforceShouldEmitExpectedMetricsWhenUserHasPrivacyData() { "bidder2", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd))); final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData()); - final Map bidderToUser = Map.of( - "bidder0", givenUserWithPrivacyData(), - "bidder1", givenUserWithPrivacyData(), - "bidder2", givenUserWithPrivacyData()); - final Set bidders = Set.of(); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder0", givenUserWithPrivacyData(), givenDeviceWithNoPrivacyData()), + givenBidderPrivacyResult("bidder1", givenUserWithPrivacyData(), givenDeviceWithNoPrivacyData()), + givenBidderPrivacyResult("bidder2", givenUserWithPrivacyData(), givenDeviceWithNoPrivacyData())); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder0", false, false, true, false, false, false); @@ -179,17 +178,17 @@ public void enforceShouldEmitExpectedMetricsWhenDeviceHavePrivacyData() { "bidder2", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo), "bidder3", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd))); - final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData()); - final Map bidderToUser = Map.of( - "bidder0", givenUserWithNoPrivacyData(), - "bidder1", givenUserWithNoPrivacyData(), - "bidder2", givenUserWithNoPrivacyData(), - "bidder3", givenUserWithNoPrivacyData(), - "bidder4", givenUserWithNoPrivacyData()); - final Set bidders = Set.of(); + final Device device = givenDeviceWithPrivacyData(); + final AuctionContext auctionContext = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder0", givenUserWithNoPrivacyData(), device), + givenBidderPrivacyResult("bidder1", givenUserWithNoPrivacyData(), device), + givenBidderPrivacyResult("bidder2", givenUserWithNoPrivacyData(), device), + givenBidderPrivacyResult("bidder3", givenUserWithNoPrivacyData(), device), + givenBidderPrivacyResult("bidder4", givenUserWithNoPrivacyData(), device)); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder0", false, false, true, false, false, true); @@ -207,14 +206,14 @@ public void enforceShouldEmitExpectedMetricsWhenUserAndDeviceDoNotHavePrivacyDat PrivacyEnforcementAction::setRemoveUserFpd, PrivacyEnforcementAction::setMaskDeviceInfo))); - final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData()); - final Map bidderToUser = Map.of( - "bidder0", givenUserWithNoPrivacyData(), - "bidder1", givenUserWithNoPrivacyData()); - final Set bidders = Set.of(); + final Device device = givenDeviceWithNoPrivacyData(); + final AuctionContext auctionContext = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder0", givenUserWithNoPrivacyData(), device), + givenBidderPrivacyResult("bidder1", givenUserWithNoPrivacyData(), device)); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder0", false, false, false, false, false, false); @@ -226,12 +225,13 @@ public void enforceShouldEmitPrivacyLmtMetric() { // give givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction())); - final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData()); - final Map bidderToUser = Map.of("bidder", givenUserWithPrivacyData()); - final Set bidders = Set.of(); + final Device device = givenDeviceWithPrivacyData(); + final AuctionContext auctionContext = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device)); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder", false, false, false, false, false, true); @@ -242,12 +242,13 @@ public void enforceShouldNotEmitPrivacyLmtMetricWhenLmtNot1() { // give givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction())); - final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData()); - final Map bidderToUser = Map.of("bidder", givenUserWithPrivacyData()); - final Set bidders = Set.of(); + final Device device = givenDeviceWithNoPrivacyData(); + final AuctionContext auctionContext = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device)); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder", false, false, false, false, false, false); @@ -259,14 +260,15 @@ public void enforceShouldNotEmitPrivacyLmtMetricWhenLmtNotEnforced() { // give givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction())); - final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData()); - final Map bidderToUser = Map.of("bidder", givenUserWithPrivacyData()); - final Set bidders = Set.of(); + final Device device = givenDeviceWithPrivacyData(); + final AuctionContext auctionContext = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device)); target = new TcfEnforcement(tcfDefinerService, userFpdTcfMask, bidderCatalog, metrics, false); // when - target.enforce(auctionContext, bidderToUser, bidders, aliases); + target.enforce(auctionContext, aliases, initialResults); // then verifyMetric("bidder", false, false, false, false, false, false); @@ -297,15 +299,15 @@ public void enforceShouldMaskUserAndDeviceWhenRestrictionsEnforcedAndLmtNotEnabl PrivacyEnforcementAction::setBlockAnalyticsReport), "bidder2", givenEnforcementAction())); - final AuctionContext context = givenAuctionContext(givenDeviceWithNoPrivacyData()); - final Map bidderToUser = Map.of( - "bidder0", givenUserWithPrivacyData(), - "bidder1", givenUserWithPrivacyData(), - "bidder2", givenUserWithPrivacyData()); - final Set bidders = Set.of("bidder0", "bidder1", "bidder2"); + final Device device = givenDeviceWithNoPrivacyData(); + final AuctionContext context = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder0", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder1", givenUserWithPrivacyData(), device), + givenBidderPrivacyResult("bidder2", givenUserWithPrivacyData(), device)); // when - final List result = target.enforce(context, bidderToUser, bidders, aliases).result(); + final List result = target.enforce(context, aliases, initialResults).result(); // then assertThat(result).containsExactlyInAnyOrder( @@ -343,12 +345,13 @@ public void enforceShouldMaskUserAndDeviceWhenRestrictionsNotEnforcedAndLmtEnabl givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction())); - final AuctionContext context = givenAuctionContext(givenDeviceWithPrivacyData()); - final Map bidderToUser = Map.of("bidder", givenUserWithPrivacyData()); - final Set bidders = Set.of("bidder"); + final Device device = givenDeviceWithPrivacyData(); + final AuctionContext context = givenAuctionContext(device); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device)); // when - final List result = target.enforce(context, bidderToUser, bidders, aliases).result(); + final List result = target.enforce(context, aliases, initialResults).result(); // then assertThat(result).containsExactly( @@ -378,6 +381,10 @@ private AuctionContext givenAuctionContext(Device device) { .build(); } + private static BidderPrivacyResult givenBidderPrivacyResult(String bidder, User user, Device device) { + return BidderPrivacyResult.builder().requestBidder(bidder).user(user).device(device).build(); + } + private static Device givenDeviceWithPrivacyData() { return Device.builder() .ip("originalDevice") @@ -394,7 +401,7 @@ private static Device givenDeviceWithNoPrivacyData() { private static User givenUserWithPrivacyData() { return User.builder() .id("originalUser") - .eids(singletonList(Eid.of(null, null, null))) + .eids(singletonList(Eid.builder().build())) .geo(Geo.builder().build()) .build(); } diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java index 14be79446d5..850c362e0d8 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java @@ -72,9 +72,9 @@ public void maskUserShouldReturnExpectedResultWhenEidsMasked() { .kwarray(emptyList()) .data(emptyList()) .eids(asList( - Eid.of("1", null, null), - Eid.of("2", null, null), - Eid.of("3", null, null))) + Eid.builder().source("1").build(), + Eid.builder().source("2").build(), + Eid.builder().source("3").build())) .geo(Geo.builder().lon(-85.34321F).lat(189.342323F).build()) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build(); @@ -92,7 +92,7 @@ public void maskUserShouldReturnExpectedResultWhenEidsMasked() { .keywords("keywords") .kwarray(emptyList()) .data(emptyList()) - .eids(singletonList(Eid.of("2", null, null))) + .eids(singletonList(Eid.builder().source("2").build())) .geo(Geo.builder().lon(-85.34321F).lat(189.342323F).build()) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build()); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index bad5e98fe8d..1e0f63b9192 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -62,6 +62,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.Targeting; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import java.util.ArrayList; @@ -154,6 +156,8 @@ public void setUp() { given(ortb2RequestFactory.restoreResultFromRejection(any())) .willAnswer(invocation -> Future.failedFuture((Throwable) invocation.getArgument(0))); given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(fpdResolver.resolveApp(any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); @@ -538,6 +542,33 @@ public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestE ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))))); } + @Test + public void shouldReturnBidRequestWithAccountPriceGranularityIfStoredBidRequestExtTargetingHasNoPriceGranularity() { + // given + givenBidRequest( + builder -> builder + .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())), + Imp.builder().build()); + + given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder().priceGranularity("low").build()) + .build())); + + // when + final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(singletonList(request)) + .extracting(BidRequest::getExt).isNotNull() + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity) + // assert that priceGranularity was set with default value and includeWinners remained unchanged + .containsExactly( + tuple(false, mapper.valueToTree(ExtPriceGranularity.of(2, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))))); + } + @Test public void shouldReturnBidRequestWithNotChangedExtRequestPrebidTargetingFields() { // given @@ -1246,10 +1277,12 @@ public void shouldReturnBidRequestWithProvidersSettingsContainsAddtlConsentIfPar final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest(); // then + final ConsentedProvidersSettings settings = ConsentedProvidersSettings.of("someConsent"); assertThat(result.getUser()) .isEqualTo(User.builder() .ext(ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("someConsent")) + .deprecatedConsentedProvidersSettings(settings) + .consentedProvidersSettings(settings) .build()) .build()); } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index ab3fa774e0c..96889137331 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -36,6 +36,9 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.geolocation.model.GeoInfo; @@ -49,14 +52,18 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; import org.prebid.server.settings.model.Account; import java.util.ArrayList; +import java.util.List; +import java.util.Map; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -102,6 +109,8 @@ public class AuctionRequestFactoryTest extends VertxTest { private DebugResolver debugResolver; @Mock(strictness = LENIENT) private GeoLocationServiceWrapper geoLocationServiceWrapper; + @Mock(strictness = LENIENT) + private BidAdjustmentsRetriever bidAdjustmentsRetriever; private AuctionRequestFactory target; @@ -158,6 +167,8 @@ public void setUp() { ((AuctionContext) invocation.getArgument(0)).getBidRequest())); given(ortb2RequestFactory.validateRequest(any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture((BidRequest) invocationOnMock.getArgument(0))); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); given(paramsResolver.resolve(any(), any(), any(), anyBoolean())) @@ -186,6 +197,7 @@ public void setUp() { .will(invocationOnMock -> invocationOnMock.getArgument(0)); given(geoLocationServiceWrapper.lookup(any())) .willReturn(Future.succeededFuture(GeoInfo.builder().vendor("vendor").build())); + given(bidAdjustmentsRetriever.retrieve(any())).willReturn(BidAdjustments.of(emptyMap())); target = new AuctionRequestFactory( Integer.MAX_VALUE, @@ -201,7 +213,8 @@ public void setUp() { auctionPrivacyContextFactory, debugResolver, jacksonMapper, - geoLocationServiceWrapper); + geoLocationServiceWrapper, + bidAdjustmentsRetriever); } @Test @@ -236,7 +249,8 @@ public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() { auctionPrivacyContextFactory, debugResolver, jacksonMapper, - geoLocationServiceWrapper); + geoLocationServiceWrapper, + bidAdjustmentsRetriever); given(routingContext.getBodyAsString()).willReturn("body"); @@ -712,6 +726,27 @@ public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementRetur assertThat(result.getGeoInfo()).isEqualTo(geoInfo); } + @Test + public void shouldReturnPopulatedBidAdjustments() { + // given + givenValidBidRequest(); + + final BidAdjustments bidAdjustments = BidAdjustments.of(Map.of( + "rule1", List.of( + ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()), + "rule2", List.of( + ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(), + ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build()))); + + given(bidAdjustmentsRetriever.retrieve(any())).willReturn(bidAdjustments); + + // when + final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); + + // then + assertThat(result.getBidAdjustments()).isEqualTo(bidAdjustments); + } + @Test public void shouldConvertBidRequestToInternalOpenRTBVersion() { // given diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java index 81680a5ac7e..41fa98842e7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java @@ -58,6 +58,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import java.util.ArrayList; @@ -1836,6 +1838,33 @@ public void shouldSetDefaultPriceGranularityIfPriceGranularityAndMediaTypePriceG BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)))))); } + @Test + public void shouldSetAccountPriceGranularityIfPriceGranularityAndMediaTypePriceGranularityIsMissing() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).ext(mapper.createObjectNode()).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder().build()) + .build())) + .build(); + + final AuctionContext givenAuctionContext = auctionContext.with(Account.builder() + .auction(AccountAuctionConfig.builder().priceGranularity("low").build()) + .build()); + + // when + final BidRequest result = target.resolve(bidRequest, givenAuctionContext, ENDPOINT, false); + + // then + assertThat(singletonList(result)) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getPricegranularity) + .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of( + BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))))); + } + @Test public void shouldNotSetDefaultPriceGranularityIfThereIsAMediaTypePriceGranularityForImpType() { // given diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index 6da1084c895..23d485e99a0 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -5,12 +5,16 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.net.impl.SocketAddressImpl; import io.vertx.ext.web.RoutingContext; @@ -36,9 +40,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.floors.PriceFloorProcessor; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; @@ -121,8 +124,6 @@ public class Ortb2RequestFactoryTest extends VertxTest { @Mock(strictness = LENIENT) private HookStageExecutor hookStageExecutor; @Mock - private PriceFloorProcessor priceFloorProcessor; - @Mock private CountryCodeMapper countryCodeMapper; @Mock private Metrics metrics; @@ -1079,6 +1080,7 @@ public void executeEntrypointHooksShouldReturnExpectedHttpRequest() { given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); given(httpServerRequest.absoluteURI()).willReturn("absoluteUri"); + given(httpServerRequest.method()).willReturn(HttpMethod.POST); given(httpServerRequest.scheme()).willReturn("https"); given(httpServerRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host")); @@ -1107,6 +1109,7 @@ public void executeEntrypointHooksShouldReturnExpectedHttpRequest() { // then final HttpRequestContext httpRequest = result.result(); assertThat(httpRequest.getAbsoluteUri()).isEqualTo("absoluteUri"); + assertThat(httpRequest.getHttpMethod()).isEqualTo(HttpMethod.POST); assertThat(httpRequest.getQueryParams()).isSameAs(updatedQueryParam); assertThat(httpRequest.getHeaders()).isSameAs(headerParams); assertThat(httpRequest.getBody()).isEqualTo("{\"app\":{\"bundle\":\"org.company.application\"}}"); @@ -1639,6 +1642,67 @@ public void enrichBidRequestWithAccountAndPrivacyDataShouldNotSetDsaFromAccountW .isSameAs(regs); } + @Test + public void removeEmptyEidsShouldNotAddWarningWhenEidsAreEmpty() { + final User givenUser = User.builder().eids(emptyList()).build(); + final BidRequest givenBidRequest = givenBidRequest(request -> request.user(givenUser)); + final List warnings = new ArrayList<>(); + + final BidRequest actualBidRequest = target.removeEmptyEids(givenBidRequest, warnings); + + assertThat(actualBidRequest.getUser().getEids()).isNull(); + assertThat(warnings).isEmpty(); + } + + @Test + public void removeEmptyEidsShouldRemoveEidsWhenNoValidEidsLeft() { + final User givenUser = User.builder() + .eids(List.of( + Eid.builder().source("source1").uids(List.of( + Uid.builder().id("").build(), + Uid.builder().id("").build())).build(), + Eid.builder().source("source2").uids(List.of(Uid.builder().id("").build())).build(), + Eid.builder().source("source3").uids(emptyList()).build())) + .build(); + + final BidRequest givenBidRequest = givenBidRequest(request -> request.user(givenUser)); + final List warnings = new ArrayList<>(); + + final BidRequest actualBidRequest = target.removeEmptyEids(givenBidRequest, warnings); + + assertThat(actualBidRequest.getUser().getEids()).isNull(); + assertThat(warnings).containsExactlyInAnyOrder( + "removed EID source1 due to empty ID", + "removed EID source1 due to empty ID", + "removed EID source2 due to empty ID", + "removed empty EID array"); + } + + @Test + public void removeEmptyEidsShouldRemoveEmptyUidsOnly() { + final User givenUser = User.builder() + .eids(List.of( + Eid.builder().source("source1").uids(List.of( + Uid.builder().id("id1").build(), + Uid.builder().id("").build())).build(), + Eid.builder().source("source2").uids(List.of(Uid.builder().id("id2").build())).build(), + Eid.builder().source("source3").uids(List.of(Uid.builder().id("").build())).build())) + .build(); + + final BidRequest givenBidRequest = givenBidRequest(request -> request.user(givenUser)); + final List warnings = new ArrayList<>(); + + final BidRequest actualBidRequest = target.removeEmptyEids(givenBidRequest, warnings); + + assertThat(actualBidRequest.getUser().getEids()).containsExactlyInAnyOrder( + Eid.builder().source("source1").uids(List.of(Uid.builder().id("id1").build())).build(), + Eid.builder().source("source2").uids(List.of(Uid.builder().id("id2").build())).build()); + + assertThat(warnings).containsExactlyInAnyOrder( + "removed EID source1 due to empty ID", + "removed EID source3 due to empty ID"); + } + private void givenTarget(int timeoutAdjustmentFactor) { target = new Ortb2RequestFactory( timeoutAdjustmentFactor, @@ -1653,7 +1717,6 @@ private void givenTarget(int timeoutAdjustmentFactor) { applicationSettings, ipAddressHelper, hookStageExecutor, - priceFloorProcessor, countryCodeMapper, metrics); } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java index 5764566ea17..475b22a5d5b 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java @@ -105,6 +105,8 @@ public void setUp() { given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); given(ortb2RequestFactory.activityInfrastructureFrom(any())) .willReturn(Future.succeededFuture()); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(ortbVersionConversionManager.convertToAuctionSupportedVersion(any())) .willAnswer(invocation -> invocation.getArgument(0)); diff --git a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java index bad068fcea4..4d0aca65fa5 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java @@ -131,7 +131,7 @@ public void convertShouldMoveUserData() { final BidRequest bidRequest = givenBidRequest(request -> request.user( User.builder() .consent("consent") - .eids(singletonList(Eid.of("source", emptyList(), null))) + .eids(singletonList(Eid.builder().source("source").uids(emptyList()).build())) .ext(mapper.convertValue(Map.of("someField", "someValue"), ExtUser.class)) .build())); @@ -148,7 +148,7 @@ public void convertShouldMoveUserData() { final ExtUser expectedUserExt = ExtUser.builder() .consent("consent") - .eids(singletonList(Eid.of("source", emptyList(), null))) + .eids(singletonList(Eid.builder().source("source").uids(emptyList()).build())) .build(); expectedUserExt.addProperty("someField", TextNode.valueOf("someValue")); assertThat(user) diff --git a/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java index 2f52722be16..db1fc112be7 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java @@ -242,7 +242,7 @@ public void convertShouldNotChangeUserConsentIfPresent() { @Test public void convertShouldMoveUserExtEidsToUserEidsIfNotPresent() { // given - final List eids = singletonList(Eid.of("source", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("source").uids(emptyList()).build()); final BidRequest bidRequest = givenBidRequest(request -> request.user( User.builder().ext(ExtUser.builder().eids(eids).build()).build())); @@ -265,7 +265,7 @@ public void convertShouldMoveUserExtEidsToUserEidsIfNotPresent() { @Test public void convertShouldNotChangeUserEidsIfPresent() { // given - final List eids = singletonList(Eid.of("source", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("source").uids(emptyList()).build()); final BidRequest bidRequest = givenBidRequest(request -> request.user( User.builder().eids(eids).ext(ExtUser.builder().eids(emptyList()).build()).build())); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java new file mode 100644 index 00000000000..0c98ff6af3b --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java @@ -0,0 +1,306 @@ +package org.prebid.server.bidadjustments; + +import org.junit.jupiter.api.Test; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.UNKNOWN; + +public class BidAdjustmentRulesValidatorTest { + + @Test + public void validateShouldDoNothingWhenBidAdjustmentsIsNull() throws ValidationException { + // when & then + BidAdjustmentRulesValidator.validate(null); + } + + @Test + public void validateShouldDoNothingWhenMediatypesIsEmpty() throws ValidationException { + // when & then + BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder().build()); + } + + @Test + public void validateShouldSkipMediatypeValidationWhenMediatypesIsNotSupported() throws ValidationException { + // given + final ExtRequestBidAdjustmentsRule invalidRule = ExtRequestBidAdjustmentsRule.builder() + .value(new BigDecimal("-999")) + .build(); + + // when & then + BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() + .mediatype(Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule))))) + .build()); + } + + @Test + public void validateShouldFailWhenBiddersAreAbsent() { + // given + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Collections.emptyMap())) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("no bidders found in banner"); + } + + @Test + public void validateShouldFailWhenDealsAreAbsent() { + // given + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", Collections.emptyMap()))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("no deals found in banner.bidderName"); + } + + @Test + public void validateShouldFailWhenRulesIsEmpty() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", null); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("no bid adjustment rules found in banner.bidderName.*"); + } + + @Test + public void validateShouldDoNothingWhenRulesAreEmpty() throws ValidationException { + // when & then + BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() + .mediatype(Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of())))) + .build()); + } + + @Test + public void validateShouldFailWhenRuleHasUnknownType() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(UNKNOWN) + .value(BigDecimal.ONE) + .currency("USD") + .build())); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=UNKNOWN, value=1, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenCpm("1", "USD"), givenCpm("1", null))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=CPM, value=1, currency=null] in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("-1", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=CPM, value=-1, currency=USD] in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("2147483647", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=CPM, value=2147483647, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenStatic("1", "USD"), givenStatic("1", null))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=STATIC, value=1, currency=null] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("-1", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=STATIC, value=-1, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("2147483647", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=STATIC, value=2147483647, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("-1"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=MULTIPLIER, value=-1, currency=null] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("100"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=MULTIPLIER, value=100, currency=null] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationException { + // given + final List givenRules = List.of( + givenMultiplier("1"), + givenCpm("2", "USD"), + givenStatic("3", "EUR")); + + final Map>> givenRulesMap = Map.of( + "bidderName", + Map.of("dealId", givenRules)); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of( + "audio", givenRulesMap, + "native", givenRulesMap, + "video-instream", givenRulesMap, + "video-outstream", givenRulesMap, + "banner", givenRulesMap, + "video", givenRulesMap, + "unknown", givenRulesMap, + "*", Map.of( + "*", Map.of("*", givenRules), + "bidderName", Map.of( + "*", givenRules, + "dealId", givenRules)))) + .build(); + + //when & then + BidAdjustmentRulesValidator.validate(givenBidAdjustments); + } + + private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java new file mode 100644 index 00000000000..2affb167eef --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java @@ -0,0 +1,878 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class BidAdjustmentsProcessorTest extends VertxTest { + + @Mock + private CurrencyConversionService currencyService; + @Mock + private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + @Mock + private BidAdjustmentsResolver bidAdjustmentsResolver; + + private BidAdjustmentsProcessor target; + + @BeforeEach + public void before() { + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + + target = new BidAdjustmentsProcessor( + currencyService, + bidAdjustmentFactorResolver, + bidAdjustmentsResolver, + jacksonMapper); + } + + @Test + public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice); + + final BigDecimal expectedPrice = new BigDecimal("123.5"); + given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBidCurrency, bidderBid -> bidderBid.getBid().getPrice()) + .containsExactly(tuple("UAH", expectedPrice)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(2.0))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2.0)); + } + + @Test + public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + final BidderError expectedError = BidderError.generic( + "Unable to convert bid currency CUR to desired ad server currency USD"); + final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).isEmpty(); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + final BidderError expectedError = BidderError.generic( + "Unable to convert bid currency CUR to desired ad server currency USD"); + final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).isEmpty(); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAndBidAdjustments() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.TEN); + final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice); + final BigDecimal expectedPrice = new BigDecimal("123.5"); + given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + final BidderSeatBid seatBid = result.getBidderResponse().getSeatBid(); + assertThat(seatBid.getBids()) + .extracting(BidderBid::getBidCurrency, bidderBid -> bidderBid.getBid().getPrice()) + .containsExactly(tuple("UAH", expectedPrice)); + assertThat(seatBid.getErrors()).isEmpty(); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(20.0))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) + .willThrow( + new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, null); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "CUR1"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "UAH"); + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); + + final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "EUR") + )) + .build(), + 1); + + final BidRequest bidRequest = BidRequest.builder() + .cur(singletonList("CUR")) + .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), + identity()))).build(); + + final BigDecimal updatedPrice = BigDecimal.valueOf(20); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("CUR"))) + .willThrow(new PreBidException("Unable to convert bid currency EUR to desired ad server currency CUR")); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("CUR")); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("EUR"), eq("CUR")); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "USD"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR"); + assertThat(result.getBidderResponse().getSeatBid().getBids()).containsOnly(expectedBidderBid); + + final BidderError expectedError = BidderError.generic( + "Unable to convert bid currency EUR to desired ad server currency CUR"); + assertThat(result.getBidderResponse().getSeatBid().getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { + // given + final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); + final BigDecimal bidder2Price = BigDecimal.valueOf(2); + final BigDecimal bidder3Price = BigDecimal.valueOf(3); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"), + givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"), + givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(Map.of("bidder1", 1), identity())), + builder -> builder.cur(singletonList("USD"))); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); + verifyNoMoreInteractions(currencyService); + + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsOnly(bidder3Price, updatedPrice, updatedPrice); + + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any()); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).dealid("dealId").build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(2.468)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(4.936)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(4.936))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(1).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_instream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlcmtEqualsOne() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().plcmt(1).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_instream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideoPlacementAndPlcmtIsMissing() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_outstream, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_outstream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidImpId() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("125") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + + verify(bidAdjustmentFactorResolver).resolve(ImpMediaType.video_outstream, givenAdjustments, "bidder"); + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(2))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_outstream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(2))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_outstream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); + + verify(bidAdjustmentsResolver, times(3)) + .resolve(any(), any(), any(), any(), any(), any()); + } + + @Test + public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD"))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsOnly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.ONE) + .dealid("dealId") + .build(), + "USD"))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .auctiontimestamp(1000L) + .currency(ExtRequestCurrency.of(null, false)) + .bidadjustmentfactors(givenAdjustments) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.ONE); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.ONE)), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + private static BidRequest givenBidRequest(List imp, + UnaryOperator bidRequestBuilderCustomizer) { + + return bidRequestBuilderCustomizer + .apply(BidRequest.builder().cur(singletonList("UAH")).imp(imp).tmax(500L)) + .build(); + } + + private static Imp givenImp(T ext, UnaryOperator impBuilderCustomizer) { + return impBuilderCustomizer.apply(Imp.builder() + .id(UUID.randomUUID().toString()) + .ext(mapper.valueToTree(singletonMap( + "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap())))) + .build(); + } + + private static BidderBid givenBidderBid(Bid bid, String currency) { + return BidderBid.of(bid, banner, currency); + } + + private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) { + return BidderBid.of(bid, type, currency); + } + + private static Map doubleMap(K key1, V value1, K key2, V value2) { + final Map map = new HashMap<>(); + map.put(key1, value1); + map.put(key2, value2); + return map; + } + + private static BidAdjustments givenBidAdjustments() { + return BidAdjustments.of(ExtRequestBidAdjustments.builder().build()); + } + + private BidderResponse givenBidderResponse(Bid bid) { + return BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(singletonList(givenBidderBid(bid, "USD"))) + .build(), + 1); + } + + private AuctionParticipation givenAuctionParticipation(BidderResponse bidderResponse, + BidRequest bidRequest) { + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidRequest(bidRequest) + .build(); + + return AuctionParticipation.builder() + .bidder("bidder") + .bidderRequest(bidderRequest) + .bidderResponse(bidderResponse) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java new file mode 100644 index 00000000000..97ca68e939e --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java @@ -0,0 +1,243 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; + +@ExtendWith(MockitoExtension.class) +public class BidAdjustmentsResolverTest extends VertxTest { + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyService; + + private BidAdjustmentsResolver target; + + @BeforeEach + public void before() { + target = new BidAdjustmentsResolver(currencyService); + + given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> { + final BigDecimal initialPrice = (BigDecimal) invocation.getArguments()[0]; + return initialPrice.multiply(BigDecimal.TEN); + }); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificMediaType() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "banner|*|*", List.of(givenStatic("15", "EUR")), + "*|*|*", List.of(givenStatic("25", "UAH")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("EUR", new BigDecimal("15"))); + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardMediaType() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "banner|*|*", List.of(givenCpm("15", "EUR")), + "*|*|*", List.of(givenCpm("25", "UAH")))); + + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.video_outstream, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-249"))); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD"); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidder() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|bidderName|*", List.of(givenMultiplier("15")), + "*|*|*", List.of(givenMultiplier("25")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("15"))); + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardBidder() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|bidderName|*", List.of(givenStatic("15", "EUR"), givenMultiplier("15")), + "*|*|*", List.of(givenStatic("25", "UAH"), givenMultiplier("25")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "anotherBidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("UAH", new BigDecimal("625"))); + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificDealId() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR")), + "*|*|*", List.of(givenCpm("25", "JPY"), givenStatic("25", "UAH")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("EUR", new BigDecimal("15"))); + verify(currencyService).convertCurrency(new BigDecimal("15"), givenBidRequest, "JPY", "USD"); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealId() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenMultiplier("15"), givenCpm("15", "EUR")), + "*|*|*", List.of(givenMultiplier("25"), givenCpm("25", "UAH")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "anotherDealId"); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-225"))); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD"); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenCpm("15", "EUR"), givenCpm("15", "JPY")), + "*|*|*", List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + null); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-499"))); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD"); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "JPY", "USD"); + } + + @Test + public void resolveShouldReturnEmptyListWhenNoMatchFound() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenStatic("15", "EUR")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + null); + + // then + assertThat(actual).isEqualTo(Price.of("USD", BigDecimal.ONE)); + verifyNoInteractions(currencyService); + } + + private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java new file mode 100644 index 00000000000..df6caa05abd --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java @@ -0,0 +1,396 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; + +public class BidAdjustmentsRetrieverTest extends VertxTest { + + private BidAdjustmentsRetriever target; + + @BeforeEach + public void before() { + target = new BidAdjustmentsRetriever(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d); + } + + @Test + public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() { + // given + final List debugMessages = new ArrayList<>(); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + null, null, debugMessages, true)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountAdjustmentsAreAbsent() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "invalid": { + "invalid": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, null, debugMessages, true)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages) + .containsOnly("bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.invalid.invalid is invalid"); + } + + @Test + public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAbsent() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, null, debugMessages, true)); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "banner|*|*", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal("0.1")) + .build()))); + + assertThat(actual).isEqualTo(expected); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAbsent() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "audio": { + "bidder": { + "*": [ + { + "adjtype": "static", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "audio|bidder|*", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(STATIC) + .currency("USD") + .value(new BigDecimal("0.1")) + .build()))); + + assertThat(actual).isEqualTo(expected); + assertThat(debugMessages) + .containsOnly("bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.*.* is invalid"); + } + + @Test + public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustmentsAreInvalid() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "audio": { + "bidder": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages).containsExactlyInAnyOrder( + "bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid", + "bid adjustment from account was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid"); + } + + @Test + public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException { + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "audio": { + "bidder": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, false)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws JsonProcessingException { + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "dealId": [ + { + "adjtype": "cpm", + "value": 0.3, + "currency": "USD" + } + ], + "*": [ + { + "adjtype": "static", + "value": 0.2, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "banner|*|dealId", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal("0.3")) + .build()), + "banner|*|*", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal("0.1")) + .build()))); + + assertThat(actual).isEqualTo(expected); + assertThat(debugMessages).isEmpty(); + } + + private static AuctionContext givenAuctionContext(ObjectNode requestBidAdjustments, + ObjectNode accountBidAdjustments, + List debugWarnings, + boolean debugEnabled) { + + return AuctionContext.builder() + .debugContext(DebugContext.of(debugEnabled, false, null)) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(requestBidAdjustments).build())) + .build()) + .account(Account.builder() + .auction(AccountAuctionConfig.builder().bidAdjustments(accountBidAdjustments).build()) + .build()) + .debugWarnings(debugWarnings) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java new file mode 100644 index 00000000000..6bc26d7ef1a --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java @@ -0,0 +1,65 @@ +package org.prebid.server.bidadjustments.model; + +import org.junit.jupiter.api.Test; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; + +public class BidAdjustmentsTest { + + @Test + public void shouldBuildRulesSet() { + // given + final List givenRules = List.of(givenRule("1"), givenRule("2")); + final Map>> givenRulesMap = Map.of( + "bidderName", + Map.of("dealId", givenRules)); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of( + "audio", givenRulesMap, + "native", givenRulesMap, + "video-instream", givenRulesMap, + "video-outstream", givenRulesMap, + "banner", givenRulesMap, + "video", givenRulesMap, + "unknown", givenRulesMap, + "*", Map.of( + "*", Map.of("*", givenRules), + "bidderName", Map.of( + "*", givenRules, + "dealId", givenRules)))) + .build(); + + // when + final BidAdjustments actual = BidAdjustments.of(givenBidAdjustments); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "audio|bidderName|dealId", givenRules, + "native|bidderName|dealId", givenRules, + "video-instream|bidderName|dealId", givenRules, + "video-outstream|bidderName|dealId", givenRules, + "banner|bidderName|dealId", givenRules, + "*|*|*", givenRules, + "*|bidderName|*", givenRules, + "*|bidderName|dealId", givenRules)); + + assertThat(actual).isEqualTo(expected); + + } + + private static ExtRequestBidAdjustmentsRule givenRule(String value) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java index 6fbb3546a02..f2e0207fc4a 100644 --- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java +++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java @@ -95,6 +95,7 @@ public void metaInfoByNameShouldReturnMetaInfoForKnownBidderIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -127,6 +128,7 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -150,6 +152,7 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -186,6 +189,7 @@ public void resolveBaseBidderShouldReturnBaseBidderName() { emptyList(), null, 0, + null, true, false, CompressionType.NONE, @@ -252,6 +256,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -269,6 +274,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -286,6 +292,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -354,6 +361,7 @@ public void nameByVendorIdShouldReturnBidderNameForVendorId() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java index 440ba94cd2f..38cad500e7b 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java @@ -170,6 +170,7 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderConfig() { null, null, 0, + null, false, false, CompressionType.GZIP, @@ -207,6 +208,7 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderAliasConfig() { null, null, 0, + null, false, false, CompressionType.GZIP, diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java index 5a677170187..d2ce145e7ef 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java @@ -33,8 +33,8 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; @@ -50,6 +50,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -60,6 +61,7 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; @@ -115,7 +117,7 @@ public void setUp() { expiredTimeout = timeoutFactory.create(clock.instant().minusMillis(1500L).toEpochMilli(), 1000L); target = new HttpBidderRequester( - httpClient, null, bidderErrorNotifier, requestEnricher, jacksonMapper); + httpClient, null, bidderErrorNotifier, requestEnricher, jacksonMapper, 0.0); given(bidder.makeBidderResponse(any(BidderCall.class), any(BidRequest.class))).willCallRealMethod(); } @@ -147,6 +149,8 @@ public void shouldReturnFailedToRequestBidsErrorWhenBidderReturnsEmptyHttpReques assertThat(bidderSeatBid.getErrors()) .containsOnly(BidderError.failedToRequestBids( "The bidder failed to generate any bid requests, but also failed to generate an error")); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -177,6 +181,8 @@ public void shouldTolerateBidderReturningErrorsAndNoHttpRequests() { assertThat(bidderSeatBid.getHttpCalls()).isEmpty(); assertThat(bidderSeatBid.getErrors()) .extracting(BidderError::getMessage).containsOnly("error1", "error2"); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -219,6 +225,9 @@ public void shouldPassStoredResponseToBidderMakeBidsMethodAndReturnSeatBids() { .extracting(HttpResponse::getBody) .isEqualTo("storedResponse"); assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -255,6 +264,9 @@ public void shouldMakeRequestToBidderWhenStoredResponseDefinedButBidderCreatesMo // then verify(httpClient, times(2)).request(any(), anyString(), any(), any(byte[].class), anyLong()); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -287,6 +299,9 @@ public void shouldSendPopulatedGetRequestWithoutBody() { // then verify(httpClient).request(any(), anyString(), any(), (byte[]) isNull(), anyLong()); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -320,6 +335,9 @@ public void shouldSendMultipleRequests() throws JsonProcessingException { // then verify(httpClient, times(2)).request(any(), anyString(), any(), any(byte[].class), anyLong()); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -350,6 +368,9 @@ public void shouldReturnBidsCreatedByBidder() { // then assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -381,6 +402,9 @@ public void shouldReturnBidsCreatedByMakeBids() { // then assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -417,6 +441,9 @@ public void shouldReturnFledgeCreatedByBidder() { // then assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); assertThat(bidderSeatBid.getFledgeAuctionConfigs()).hasSameElementsAs(fledgeAuctionConfigs); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -450,6 +477,9 @@ public void shouldCompressRequestBodyIfContentEncodingHeaderIsGzip() { final ArgumentCaptor actualRequestBody = ArgumentCaptor.forClass(byte[].class); verify(httpClient).request(any(), anyString(), any(), actualRequestBody.capture(), anyLong()); assertThat(actualRequestBody.getValue()).isNotSameAs(EMPTY_BYTE_BODY); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -476,7 +506,8 @@ public void processBids(List bids) { }, bidderErrorNotifier, requestEnricher, - jacksonMapper); + jacksonMapper, + 0.0); final BidRequest bidRequest = bidRequestWithDeals("deal1", "deal2"); final BidderRequest bidderRequest = BidderRequest.builder() @@ -544,6 +575,9 @@ public void processBids(List bids) { verify(bidder, times(2)).makeBidderResponse(any(), any()); assertThat(bidderSeatBid.getBids()).containsOnly(bidderBidDeal1, bidderBidDeal2); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -590,6 +624,9 @@ public void shouldFinishWhenAllDealRequestsAreFinishedAndNoDealsProvided() { verify(bidder, times(4)).makeBidderResponse(any(), any()); assertThat(bidderSeatBid.getBids()).contains(bidderBid, bidderBid, bidderBid, bidderBid); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -654,6 +691,9 @@ public void shouldReturnFullDebugInfoIfDebugEnabled() throws JsonProcessingExcep .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) .status(200) .build()); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -717,8 +757,8 @@ public void shouldReturnRecordBidRejections() throws JsonProcessingException { // then verify(bidRejectionTracker, atLeast(1)).succeed(secondRequestBids); - verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); - verify(bidRejectionTracker).reject(singleton("3"), BidRejectionReason.OTHER_ERROR); + verify(bidRejectionTracker).rejectImps(singleton("1"), BidRejectionReason.REQUEST_BLOCKED_GENERAL); + verify(bidRejectionTracker).rejectImps(singleton("3"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); } @Test @@ -761,6 +801,9 @@ public void shouldNotReturnSensitiveHeadersInFullDebugInfo() assertThat(bidderSeatBid.getHttpCalls()) .extracting(ExtHttpCall::getRequestheaders) .containsExactly(singletonMap("headerKey", singletonList("headerValue"))); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } @Test @@ -775,7 +818,9 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndGlobalTimeoutAlreadyExp .uri("uri1") .headers(headers) .payload(givenBidRequest) - .body(requestBody))), + .body(requestBody) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId) + .collect(Collectors.toSet())))), emptyList())); given(requestEnricher.enrichHeaders(anyString(), any(), any(), any(), any())).willReturn(headers); @@ -804,6 +849,8 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndGlobalTimeoutAlreadyExp .requestbody(mapper.writeValueAsString(givenBidRequest)) .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) .build()); + + verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT); } @Test @@ -817,6 +864,7 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() throw .uri("uri1") .headers(headers) .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) .body(requestBody))), emptyList())); @@ -849,6 +897,8 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() throw .requestbody(mapper.writeValueAsString(givenBidRequest)) .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) .build()); + + verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_GENERAL); } @Test @@ -862,6 +912,7 @@ public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() throws JsonP .uri("uri1") .headers(headers) .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) .body(requestBody))), emptyList())); @@ -899,15 +950,75 @@ public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() throws JsonP assertThat(bidderSeatBid.getErrors()) .extracting(BidderError::getMessage) .containsExactly("Unexpected status code: 500. Run with request.test = 1 for more info"); + + verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); } @Test - public void shouldTolerateAlreadyExpiredGlobalTimeout() { + public void shouldReturnFullDebugInfoIfDebugEnabledAndBidderIsUnreachable() throws JsonProcessingException { // given - given(bidder.makeHttpRequests(any())).willReturn(Result.of( - singletonList(givenSimpleHttpRequest(identity())), + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().add("headerKey", "headerValue"); + final BidRequest givenBidRequest = givenBidRequest(identity()); + final byte[] requestBody = mapper.writeValueAsBytes(givenBidRequest); + given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( + givenSimpleHttpRequest(httpRequestBuilder -> httpRequestBuilder + .uri("uri1") + .headers(headers) + .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) + .body(requestBody))), emptyList())); + given(requestEnricher.enrichHeaders(anyString(), any(), any(), any(), any())).willReturn(headers); + + givenHttpClientReturnsResponses(HttpClientResponse.of(503, null, "responseBody1")); + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidder("bidder") + .bidRequest(BidRequest.builder().build()) + .build(); + + // when + final BidderSeatBid bidderSeatBid = + target + .requestBids( + bidder, + bidderRequest, + bidRejectionTracker, + timeout, + CaseInsensitiveMultiMap.empty(), + bidderAliases, + true) + .result(); + + // then + assertThat(bidderSeatBid.getHttpCalls()).containsExactly( + ExtHttpCall.builder() + .uri("uri1") + .requestbody(mapper.writeValueAsString(givenBidRequest)) + .responsebody("responseBody1") + .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) + .status(503).build()); + + assertThat(bidderSeatBid.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Unexpected status code: 503. Run with request.test = 1 for more info"); + + verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + } + + @Test + public void shouldTolerateAlreadyExpiredGlobalTimeout() throws JsonProcessingException { + // given + final BidRequest givenBidRequest = givenBidRequest(identity()); + final byte[] requestBody = mapper.writeValueAsBytes(givenBidRequest); + given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( + givenSimpleHttpRequest(httpRequestBuilder -> httpRequestBuilder + .uri("uri1") + .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) + .body(requestBody))), + emptyList())); final BidderRequest bidderRequest = BidderRequest.builder() .bidder("bidder") .bidRequest(BidRequest.builder().build()) @@ -929,6 +1040,8 @@ public void shouldTolerateAlreadyExpiredGlobalTimeout() { .extracting(BidderError::getMessage) .containsOnly("Timeout has been exceeded"); verifyNoInteractions(httpClient); + + verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT); } @Test @@ -960,7 +1073,6 @@ public void shouldNotifyBidderOfTimeout() { // then verify(bidderErrorNotifier).processTimeout(any(), same(bidder)); - verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.TIMED_OUT); } @Test @@ -968,17 +1080,19 @@ public void shouldTolerateMultipleErrors() { // given given(bidder.makeHttpRequests(any())).willReturn(Result.of(asList( // this request will fail with response exception - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("1"))), // this request will fail with timeout - givenSimpleHttpRequest(identity()), - // this request will fail with 500 status - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("2"))), + // this request will fail with 503 status + givenSimpleHttpRequest(builder -> builder.impIds(singleton("3"))), // this request will fail with 400 status - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("4"))), + // this request will fail with 404 status + givenSimpleHttpRequest(builder -> builder.impIds(singleton("5"))), // this request will get 204 status - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("6"))), // finally this request will succeed - givenSimpleHttpRequest(identity())), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("7")))), singletonList(BidderError.badInput("makeHttpRequestsError")))); when(requestEnricher.enrichHeaders(anyString(), any(), any(), any(), any())) .thenAnswer(invocation -> MultiMap.caseInsensitiveMultiMap()); @@ -987,10 +1101,12 @@ public void shouldTolerateMultipleErrors() { .willReturn(Future.failedFuture(new RuntimeException("Response exception"))) // simulate timeout for the second request .willReturn(Future.failedFuture(new TimeoutException("Timeout exception"))) - // simulate 500 status - .willReturn(Future.succeededFuture(HttpClientResponse.of(500, null, EMPTY))) + // simulate 503 status + .willReturn(Future.succeededFuture(HttpClientResponse.of(503, null, EMPTY))) // simulate 400 status .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, EMPTY))) + // simulate 400 status + .willReturn(Future.succeededFuture(HttpClientResponse.of(404, null, EMPTY))) // simulate 204 status .willReturn(Future.succeededFuture(HttpClientResponse.of(204, null, EMPTY))) // simulate 200 status @@ -1027,9 +1143,19 @@ public void shouldTolerateMultipleErrors() { BidderError.badInput("makeHttpRequestsError"), BidderError.generic("Response exception"), BidderError.timeout("Timeout exception"), - BidderError.badServerResponse("Unexpected status code: 500. Run with request.test = 1 for more info"), + BidderError.badServerResponse("Unexpected status code: 503. Run with request.test = 1 for more info"), BidderError.badInput("Unexpected status code: 400. Run with request.test = 1 for more info"), + BidderError.badServerResponse("Unexpected status code: 404. Run with request.test = 1 for more info"), BidderError.badServerResponse("makeBidsError")); + + verify(bidRejectionTracker).rejectImps(singleton("1"), BidRejectionReason.ERROR_GENERAL); + verify(bidRejectionTracker).rejectImps(singleton("2"), BidRejectionReason.ERROR_TIMED_OUT); + verify(bidRejectionTracker).rejectImps(singleton("3"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + verify(bidRejectionTracker).rejectImps(singleton("4"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + verify(bidRejectionTracker).rejectImps(singleton("5"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + verify(bidRejectionTracker, never()).rejectImps(eq(singleton("6")), any()); + verify(bidRejectionTracker, never()).rejectImps(eq(singleton("7")), any()); + } @Test @@ -1060,6 +1186,9 @@ public void shouldNotMakeBidsIfResponseStatusIs204() { // then verify(bidder, never()).makeBidderResponse(any(), any()); verify(bidder, never()).makeBids(any(), any()); + + verify(bidRejectionTracker, never()).rejectImp(anyString(), any()); + verify(bidRejectionTracker, never()).rejectImps(anyList(), any()); } private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { diff --git a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java index aa7bf15349d..c911aed14c5 100644 --- a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java @@ -25,6 +25,7 @@ import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusNetBid; @@ -38,6 +39,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; @@ -266,8 +268,7 @@ public void makeHttpRequestsShouldPopulateMetaDataUsiFromUserIdWhenBothUidIdAndU request -> request.user(User.builder() .id("userId") .ext(ExtUser.builder() - .eids(List.of(Eid.of(null, - List.of(Uid.of("eidsId", null, null)), null))) + .eids(List.of(Eid.builder().uids(List.of(Uid.builder().id("eidsId").build())).build())) .build()) .build()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), @@ -292,8 +293,7 @@ public void makeHttpRequestsShouldPopulateMetaDataUsiWhenUserExtEidsUidIdPresent request -> request.user(User.builder() .id(null) .ext(ExtUser.builder() - .eids(List.of(Eid.of(null, - List.of(Uid.of("eidsId", null, null)), null))) + .eids(List.of(Eid.builder().uids(List.of(Uid.builder().id("eidsId").build())).build())) .build()) .build()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), @@ -750,6 +750,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( .creativeId("creativeId") .lineItemId("lineItemId") .dealId("dealId") + .advertiser(AdnuntiusAdvertiser.of(null, "name")) .destinationUrls(Map.of( "key1", "https://www.domain1.com/uri", "key2", "http://www.domain2.dt/uri")))), @@ -760,6 +761,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( .lineItemId("lineItemId") .dealId("dealId") .html("dealHtml") + .advertiser(AdnuntiusAdvertiser.of("legalName", "name")) .destinationUrls(Map.of( "key1", "https://www.domain1.com/uri", "key2", "http://www.domain2.dt/uri")))))); @@ -785,6 +787,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( assertThat(bid).extracting(Bid::getPrice).isEqualTo(BigDecimal.valueOf(1000)); assertThat(bid).extracting(Bid::getAdomain).asList() .containsExactlyInAnyOrder("domain1.com", "domain2.dt"); + assertThat(bid).extracting(Bid::getExt).isNull(); }); assertThat(bidderBid).extracting(BidderBid::getType).isEqualTo(BidType.banner); assertThat(bidderBid).extracting(BidderBid::getBidCurrency).isEqualTo("USD"); @@ -792,6 +795,60 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( assertThat(result.getErrors()).isEmpty(); } + @Test + public void makeBidsShouldReturnTwoBidWithDsaFromDealsAndAdsWhenAdsAndDealsIsSpecifiedAndDsaReturned() + throws JsonProcessingException { + + // given + final BidderCall httpCall = givenHttpCall(givenAdsUnitWithDealsAndAds( + "auId", + List.of(givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .adId("adId") + .creativeId("creativeId") + .lineItemId("lineItemId") + .dealId("dealId") + .advertiser(AdnuntiusAdvertiser.of(null, "name")) + .destinationUrls(Map.of( + "key1", "https://www.domain1.com/uri", + "key2", "http://www.domain2.dt/uri")))), + List.of(givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .adId("adId") + .creativeId("creativeId") + .lineItemId("lineItemId") + .dealId("dealId") + .html("dealHtml") + .advertiser(AdnuntiusAdvertiser.of("legalName", "name")) + .destinationUrls(Map.of( + "key1", "https://www.domain1.com/uri", + "key2", "http://www.domain2.dt/uri")))))); + + final ExtRegsDsa dsa = ExtRegsDsa.of(1, 0, 2, null); + final BidRequest bidRequest = givenBidRequest( + request -> request.regs(Regs.builder().ext(ExtRegs.of(null, null, null, dsa)).build()), + givenImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).hasSize(2) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly( + mapper.createObjectNode().set("dsa", mapper.createObjectNode() + .put("paid", "name") + .put("behalf", "name") + .put("adrender", 0)), + mapper.createObjectNode().set("dsa", mapper.createObjectNode() + .put("paid", "legalName") + .put("behalf", "legalName") + .put("adrender", 0))); + + assertThat(result.getErrors()).isEmpty(); + } + @Test public void makeBidsShouldReturnErrorIfCreativeHeightOfSomeAdIsAbsent() throws JsonProcessingException { // given @@ -947,7 +1004,7 @@ private static String givenExpectedUrl(Boolean noCookies) { } private static String buildExpectedUrl(Integer gdpr, String consent, Boolean noCookies) { - final StringBuilder expectedUri = new StringBuilder("https://test.domain.dm/uri?format=json&tzo=-300"); + final StringBuilder expectedUri = new StringBuilder("https://test.domain.dm/uri?format=prebid&tzo=-300"); if (gdpr != null) { expectedUri.append("&gdpr=").append(HttpUtil.encodeUrl(gdpr.toString())); } diff --git a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java index acf7fdc44df..5ff7e138eed 100644 --- a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidder.adtarget.proto.AdtargetImpExt; +import org.prebid.server.bidder.adtarget.proto.ExtImpAdtargetBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -59,7 +60,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .imp(singletonList(bidRequest.getImp().getFirst().toBuilder() .bidfloor(BigDecimal.valueOf(3)) .ext(mapper.valueToTree(AdtargetImpExt.of( - ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtImpAdtargetBidRequest.of(15, 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); assertThat(result.getErrors()).isEmpty(); @@ -133,7 +134,7 @@ public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() { .id("impId2") .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -157,7 +158,7 @@ public void makeHttpRequestShouldReturnWithBidFloorPopulatedFromImpWhenIsMissedI .imp(singletonList(Imp.builder() .banner(Banner.builder().build()) .bidfloor(BigDecimal.valueOf(16)) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, null)))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, null)))) .build())) .build(); @@ -179,12 +180,12 @@ public void makeHttpRequestShouldReturnTwoHttpRequestsWhenTwoImpsHasDifferentSou .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(16, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("16", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -224,12 +225,12 @@ public void makeHttpRequestShouldReturnOneHttpRequestForTowImpsWhenImpsHasSameSo .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -409,7 +410,7 @@ private static Imp givenImp(Function impCustomiz .id("impId") .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3)))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3)))))) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java b/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java index 7b313298441..863d94806e1 100644 --- a/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidder.adtelligent.proto.AdtelligentImpExt; +import org.prebid.server.bidder.adtelligent.proto.ExtImpAdtelligentBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -51,7 +52,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .imp(singletonList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))).build())) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))).build())) .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) .build()) @@ -77,7 +78,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .banner(Banner.builder().build()) .bidfloor(BigDecimal.valueOf(3)) .ext(mapper.valueToTree(AdtelligentImpExt.of( - ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtImpAdtelligentBidRequest.of(15, 1, 2, BigDecimal.valueOf(3))))) .build())) .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) @@ -93,7 +94,7 @@ public void makeHttpRequestShouldReturnErrorMessageWhenMediaTypeWasNotDefined() .imp(singletonList(Imp.builder() .id("impId") .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))).build())) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))).build())) .build(); // when @@ -136,7 +137,7 @@ public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() { .id("impId2") .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -160,7 +161,7 @@ public void makeHttpRequestShouldReturnWithBidFloorPopulatedFromImpWhenIsMissedI .imp(singletonList(Imp.builder() .banner(Banner.builder().build()) .bidfloor(BigDecimal.valueOf(16)) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, null)))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, null)))) .build())) .build(); @@ -182,12 +183,12 @@ public void makeHttpRequestShouldReturnTwoHttpRequestsWhenTwoImpsHasDifferentSou .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(16, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("16", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -206,12 +207,12 @@ public void makeHttpRequestShouldReturnOneHttpRequestForTowImpsWhenImpsHasSameSo .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); diff --git a/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java b/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java new file mode 100644 index 00000000000..2d66b2f28ac --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java @@ -0,0 +1,271 @@ +package org.prebid.server.bidder.adtonos; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adtonos.ExtImpAdtonos; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +public class AdtonosBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://randomurl.com?param={{PublisherId}}"; + + private final AdtonosBidder target = new AdtonosBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AdtonosBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateExpectedUrl() { + // given + final ExtImpAdtonos impExt = ExtImpAdtonos.of("publisherId"); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, impExt)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://randomurl.com?param=publisherId"); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfMTypeIsOne() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(1).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfMTypeIsTwo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(2).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidIfMTypeIsThree() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(3).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfMTypeIsFour() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(4).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidsForAudioImpsIfMTypeMissed() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(impBuilder -> + impBuilder.audio(Audio.builder().build())))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnVideoBidsForVideoImpsIfMTypeMissed() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(impBuilder -> + impBuilder.video(Video.builder().build())))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorsForBidsThatDoesNotMatchSupportedMediaTypes() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(identity()))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().id("456").impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("Unsupported bidtype for bid: 456"); + } + + @Test + public void makeBidsShouldReturnErrorsForBidsThatDoesNotContainMTypeAndImpMatch() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(identity()))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("789").build(), + Bid.builder().id("123").mtype(1).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().id("123").mtype(1).build(), banner, "USD")); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("Failed to find impression: 789"); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(impCustomizer.apply(Imp.builder().id("123")).build()))) + .build(); + } + + private static BidResponse givenBidResponse(Bid... bids) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(List.of(bids)) + .build())) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("123")) + .banner(Banner.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtonos.of("testPubId")))) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java b/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java index 52495c635da..ab68ca9a517 100644 --- a/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java @@ -67,7 +67,6 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { @Test public void makeHttpRequestsShouldReturnErrorOfEveryNotValidImp() { // given - final BidRequest bidRequest = BidRequest.builder() .imp(asList(Imp.builder() .id("123") diff --git a/src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java new file mode 100644 index 00000000000..3e5891e02b2 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java @@ -0,0 +1,382 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bidmatic.ExtImpBidmatic; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class BidmaticBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com"; + + private BidmaticBidder target; + + @BeforeEach + public void before() { + target = new BidmaticBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + // when and then + assertThatIllegalArgumentException().isThrownBy(() -> new BidmaticBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(bidderError -> { + assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(bidderError.getMessage()).startsWith("Cannot deserialize value"); + }); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerSourceId() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("1")), + imp -> imp.id("givenImp2").ext(givenImpExt("1")), + imp -> imp.id("givenImp3").ext(givenImpExt("2"))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(payload -> payload.getImp().stream().map(Imp::getId).collect(Collectors.toList())) + .containsExactlyInAnyOrder(List.of("givenImp1", "givenImp2"), List.of("givenImp3")); + } + + @Test + public void makeHttpRequestsShouldHaveImpIdsAndCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("1")), + imp -> imp.id("givenImp2").ext(givenImpExt("1")), + imp -> imp.id("givenImp3").ext(givenImpExt("2"))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds, HttpRequest::getUri) + .containsExactlyInAnyOrder( + tuple(Set.of("givenImp1", "givenImp2"), "https://test-url.com?source=1"), + tuple(Set.of("givenImp3"), "https://test-url.com?source=2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnImpWithUpdatedBidFloorWhenImpExtHasValidBidFloor() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").bidfloor(BigDecimal.TEN).ext(givenImpExt("1", BigDecimal.ONE)), + imp -> imp.id("givenImp2").bidfloor(BigDecimal.TEN).ext(givenImpExt("1", BigDecimal.ZERO))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId, Imp::getBidfloor) + .containsOnly(tuple("givenImp1", BigDecimal.ONE), tuple("givenImp2", BigDecimal.TEN)); + } + + @Test + public void makeHttpRequestsShouldReturnImpWithUpdatedExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("1", new BigDecimal("10.37")))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + + final ObjectNode expectedNode = mapper.createObjectNode(); + expectedNode.set("source", IntNode.valueOf(1)); + expectedNode.set("placementId", IntNode.valueOf(2)); + expectedNode.set("siteId", IntNode.valueOf(3)); + expectedNode.set("bidFloor", DecimalNode.valueOf(new BigDecimal("10.37"))); + final ObjectNode expectedImpExt = mapper.createObjectNode().set("bidmatic", expectedNode); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedImpExt); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("impId2").ext(givenImpExt("string")), + imp -> imp.id("impId3")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("impId3"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenSourceIdCanNotBeParsedToInt() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("string"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsOnly(BidderError.badInput("Cannot parse sourceId=string to int")); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidResponseHasEmptySeatbids() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString( + BidResponse.builder().seatbid(emptyList()).build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").banner(Banner.builder().build())), + givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(bannerBid.toBuilder().mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").video(Video.builder().build())), + givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(videoBid.toBuilder().mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBid() throws JsonProcessingException { + // given + final Bid audioBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").audio(Audio.builder().build())), + givenBidResponse(audioBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(audioBid.toBuilder().mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").xNative(Native.builder().build())), + givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(nativeBid.toBuilder().mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidIsNotInRequest() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(bid -> bid.id("bidId").impid("anotherImpId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").xNative(Native.builder().build())), + givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse( + "ignoring bid id=bidId, request doesn't contain any impression with id=anotherImpId")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(BidmaticBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .bidfloor(BigDecimal.TEN) + .bidfloorcur("USD") + .ext(mapper.valueToTree(ExtPrebid.of( + null, + ExtImpBidmatic.of("100", 1, 2, BigDecimal.TEN))))) + .build(); + } + + private static ObjectNode givenImpExt(String sourceId) { + return givenImpExt(sourceId, BigDecimal.TWO); + } + + private static ObjectNode givenImpExt(String sourceId, BigDecimal bidFloor) { + return mapper.valueToTree(ExtPrebid.of( + null, + ExtImpBidmatic.of(sourceId, 2, 3, bidFloor))); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(asList(bids)).build())) + .cur("USD") + .build()); + } + + private static Bid givenBid(UnaryOperator bidCustomizer) { + return bidCustomizer.apply(Bid.builder()).build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java b/src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java similarity index 91% rename from src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java rename to src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java index 283047ecaff..aa19d5b55e6 100644 --- a/src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.bizzclick; +package org.prebid.server.bidder.blasto; import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; @@ -19,7 +19,7 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.bizzclick.ExtImpBizzclick; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -34,19 +34,19 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.groups.Tuple.tuple; -public class BizzclickBidderTest extends VertxTest { +public class BlastoBidderTest extends VertxTest { - private static final String ENDPOINT = "https://{{Host}}/uri?source={{SourceId}}&account={{AccountID}}"; + private static final String ENDPOINT = "https://test.com/uri?source={{SourceId}}&account={{AccountID}}"; private static final String DEFAULT_HOST = "host"; private static final String DEFAULT_ACCOUNT_ID = "accountId"; private static final String DEFAULT_SOURCE_ID = "sourceId"; private static final String DEFAULT_PLACEMENT_ID = "placementId"; - private final BizzclickBidder target = new BizzclickBidder(ENDPOINT, jacksonMapper); + private final BlastoBidder target = new BlastoBidder(ENDPOINT, jacksonMapper); @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new BizzclickBidder("incorrect_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> new BlastoBidder("incorrect_url", jacksonMapper)); } @Test @@ -206,33 +206,7 @@ public void makeHttpRequestsShouldCreateSingleRequestWithExpectedUri() { // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsExactly( - String.format("https://%s/uri?source=%s&account=%s", - DEFAULT_HOST, - DEFAULT_SOURCE_ID, - DEFAULT_ACCOUNT_ID)); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldCreateSingleRequestWithExpectedAlternativeUri() { - // given - final String expectedDefaultHost = "us-e-node1"; - final BidRequest bidRequest = givenBidRequest( - givenImp(expectedDefaultHost, DEFAULT_ACCOUNT_ID, DEFAULT_PLACEMENT_ID, null) - ); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly( - String.format("https://%s/uri?source=%s&account=%s", - expectedDefaultHost, - DEFAULT_PLACEMENT_ID, - DEFAULT_ACCOUNT_ID)); + .containsExactly("https://test.com/uri?source=sourceId&account=accountId"); assertThat(result.getErrors()).isEmpty(); } @@ -448,7 +422,7 @@ private Imp givenImp(UnaryOperator impCustomizer) { } private Imp givenImp() { - final ExtPrebid ext = ExtPrebid.of(null, ExtImpBizzclick.of( + final ExtPrebid ext = ExtPrebid.of(null, ExtImpBlasto.of( DEFAULT_HOST, DEFAULT_ACCOUNT_ID, DEFAULT_PLACEMENT_ID, DEFAULT_SOURCE_ID )); return givenImp(imp -> imp.ext(mapper.valueToTree(ext))); @@ -456,7 +430,7 @@ private Imp givenImp() { private Imp givenImp(String host, String accountId, String placementId, String sourceId) { final ExtPrebid ext = ExtPrebid.of( - null, ExtImpBizzclick.of(host, accountId, placementId, sourceId) + null, ExtImpBlasto.of(host, accountId, placementId, sourceId) ); return givenImp(imp -> imp.ext(mapper.valueToTree(ext))); } diff --git a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java index 35e12ad3510..3ac89edc67a 100644 --- a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java @@ -146,7 +146,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtHasNoSiteId() { impBuilder -> impBuilder .id("123") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of(12, null, BigDecimal.ONE))))); + ExtImpConnectAd.of("12", null, BigDecimal.ONE))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -164,7 +164,7 @@ public void impSecureShouldBeOneIfSitePageStartsFromHttps() { impBuilder -> impBuilder .id("123") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of(12, 1, BigDecimal.ONE))))); + ExtImpConnectAd.of("12", "1", BigDecimal.ONE))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -202,7 +202,7 @@ private static Imp givenImp(Function impCustomiz .w(14) .h(15).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of(12, 12, BigDecimal.ONE))))) + ExtImpConnectAd.of("12", "12", BigDecimal.ONE))))) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java b/src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java new file mode 100644 index 00000000000..0a08b03a6aa --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java @@ -0,0 +1,337 @@ +package org.prebid.server.bidder.copper6ssp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.copper6ssp.proto.Copper6SspImpExtBidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.copper6ssp.ImpExtCopper6Ssp; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class Copper6SspBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final Copper6SspBidder target = new Copper6SspBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new Copper6SspBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("validImpId")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("validImpId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenNoValidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Cannot deserialize value of type"); + }); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ImpExtCopper6Ssp.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ImpExtCopper6Ssp.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("impId").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("impId").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(Copper6SspBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ImpExtCopper6Ssp.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExt(UnaryOperator impExt) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExt.apply(Copper6SspImpExtBidder.builder()).build(), + JsonNode.class)); + } +} diff --git a/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java b/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java index af44957b2b6..09f2f113fb1 100644 --- a/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java @@ -15,10 +15,12 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.util.List; import java.util.Set; @@ -73,12 +75,12 @@ public void makeHttpRequestsShouldEncodePassedBidRequest() { } @Test - public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + public void makeBidderResponseShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given final BidderCall httpCall = givenHttpCall("invalid"); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).hasSize(1) @@ -86,127 +88,126 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); }); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + public void makeBidderResponseShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsEmpty() throws JsonProcessingException { + public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsEmpty() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( mapper.writeValueAsString(BidResponse.builder().seatbid(emptyList()).build())); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnErrorWhenBidExtPrebidTypeIsNotPresent() throws JsonProcessingException { + public void makeBidderResponseShouldReturnErrorWhenBidExtPrebidTypeIsNotPresent() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.impid("123")))); + givenBidResponse(bid -> bid.impid("123"))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()) .containsExactly(BidderError.badServerResponse("Missing ext.prebid.type in bid for impression : 123.")); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + public void makeBidderResponseShouldReturnBannerBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner))))); + givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getType) .containsExactly(BidType.banner); } @Test - public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + public void makeBidderResponseShouldReturnVideoBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.ext(givenBidExt(BidType.video))))); + givenBidResponse(bid -> bid.ext(givenBidExt(BidType.video)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getType) .containsExactly(BidType.video); } @Test - public void makeBidsShouldReturnBidWithCurFromResponse() throws JsonProcessingException { + public void makeBidderResponseShouldReturnBidWithCurFromResponse() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner))))); + givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getBidCurrency) .containsExactly("CUR"); } @Test - public void makeBidsShouldReturnBidWithNetworkNameFromExtPrebid() throws JsonProcessingException { + public void makeBidderResponseShouldReturnBidWithNetworkNameFromExtPrebid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse( - bid -> bid + givenBidResponse(bid -> bid .impid("123") - .ext(givenBidExtWithNetwork("anyNetworkName"))))); + .ext(givenBidExtWithNetwork("anyNetworkName")))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getBid) .extracting(Bid::getExt) .extracting(ext -> ext.get("meta")) @@ -214,32 +215,82 @@ public void makeBidsShouldReturnBidWithNetworkNameFromExtPrebid() throws JsonPro } @Test - public void makeBidsShouldReturnEmptyNetworkNameWhenBidExtPrebidNotContainNetworkName() + public void makeBidderResponseShouldReturnEmptyNetworkNameWhenBidExtPrebidNotContainNetworkName() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse( - bid -> bid.ext(givenBidExtWithNetwork(null))))); + givenBidResponse(bid -> bid.ext(givenBidExtWithNetwork(null)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getBid) .extracting(Bid::getExt) .extracting(ext -> ext.get("meta")) .doesNotContain(mapper.createObjectNode().put("networkName", "anyNetworkName")); } - private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { - return BidResponse.builder() + @Test + public void makeBidderResponseShouldReturnFledgeConfigs() throws JsonProcessingException { + // given + final CriteoBidResponse bidResponseWithFledge = CriteoBidResponse.builder() + .ext(CriteoExtBidResponse.of(List.of( + CriteoIgiExtBidResponse.of("imp_id1", List.of( + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty1", "value1")), + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty2", "value2")))), + CriteoIgiExtBidResponse.of("imp_id2", List.of( + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty3", "value3")), + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty4", "value4"))))))) + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponseWithFledge)); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getFledgeAuctionConfigs()) + .containsExactlyInAnyOrder( + FledgeAuctionConfig.builder() + .bidder("criteo") + .impId("imp_id1") + .config(mapper.createObjectNode().put("proterty1", "value1")) + .build(), + FledgeAuctionConfig.builder() + .bidder("criteo") + .impId("imp_id2") + .config(mapper.createObjectNode().put("proterty3", "value3")) + .build()); + } + + @Test + public void makeBidsShouldFail() throws JsonProcessingException { + //given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.generic("Deprecated adapter method invoked")); + } + + private static String givenBidResponse(UnaryOperator bidCustomizer) + throws JsonProcessingException { + + return mapper.writeValueAsString(BidResponse.builder() .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .cur("CUR") - .build(); + .build()); } private static BidderCall givenHttpCall(String body) { diff --git a/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java b/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java index a88bf18e406..fe403f14fb9 100644 --- a/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java @@ -88,16 +88,22 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldReturnErrorWhenBidFloorIsMissing() { + public void makeHttpRequestsShouldSetDefaultCurrencyEvenWhenBidfloorIsAbsent() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null)); + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null).bidfloorcur("EUR")); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).containsOnly(BidderError.badInput("BidFloor should be defined")); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(null, "USD")); + + verifyNoInteractions(currencyConversionService); } @Test @@ -156,8 +162,7 @@ public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { // given final BidRequest bidRequest = givenBidRequest( imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), - imp -> imp.id("impId2").bidfloor(null), - imp -> imp.id("impId3")); + imp -> imp.id("impId2")); //when final Result>> result = target.makeHttpRequests(bidRequest); @@ -167,7 +172,7 @@ public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getId) - .containsExactly("impId3"); + .containsExactly("impId2"); } @Test diff --git a/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java b/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java index 91f0ab88d8c..872a04c0a96 100644 --- a/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -41,6 +42,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -610,6 +612,25 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } + @Test + public void makeBidsShouldReturnAudioBidIfRequestImpHasAudio() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidRequest(builder -> builder.id("123") + .audio(Audio.builder().build()) + .banner(Banner.builder().build())), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); + } + @Test public void makeBidsShouldUpdateBidWithUUIDIfGenerateBidIdIsTrue() throws JsonProcessingException { // given diff --git a/src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java b/src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java new file mode 100644 index 00000000000..34a74ff904a --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java @@ -0,0 +1,269 @@ +package org.prebid.server.bidder.escalax; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.escalax.ExtImpEscalax; + +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER; +import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; +import static org.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class EscalaxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com?k={{AccountID}}&name={{SourceId}}"; + + private final EscalaxBidder target = new EscalaxBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new EscalaxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldRemoveOnlyFirstImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1"), + imp -> imp.id("impId2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(imps -> imps.getFirst()) + .extracting(Imp::getExt) + .containsOnlyNulls(); + + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(imps -> imps.get(1)) + .extracting(Imp::getExt) + .doesNotContainNull(); + } + + @Test + public void makeHttpRequestsShouldMakeSingleRequestForAllImps() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(2); + + assertThat(result.getValue()).hasSize(1) + .flatExtracting(HttpRequest::getImpIds) + .containsOnly("givenImp1", "givenImp2"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get(X_OPENRTB_VERSION_HEADER)) + .isEqualTo("2.5")) + .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)) + .isEqualTo("ua")) + .satisfies(headers -> assertThat(headers.getAll(X_FORWARDED_FOR_HEADER)) + .isEqualTo(List.of("ipv6", "ip"))); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnHttpRequestWithCorrectUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?k=accountId&name=sourceId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(builder -> builder + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Error parsing escalaxExt - Cannot deserialize")); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseCanNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> actual = target.makeBids(httpCall, null); + + // then + assertThat(actual.getValue()).isEmpty(); + assertThat(actual.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseDoesNotHaveSeatBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> actual = target.makeBids(httpCall, null); + + // then + assertThat(actual.getValue()).isEmpty(); + assertThat(actual.getErrors()).containsExactly(badServerResponse("Empty SeatBid array")); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("1").mtype(1))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("1").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("2").mtype(2))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("2").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnBidsSuccessfully() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("4").mtype(4))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(4).impid("4").build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenImpTypeIsNotSupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("3").mtype(3))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(badServerResponse("unsupported MType 3")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) + .imp(Arrays.stream(impCustomizers).map(EscalaxBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpEscalax.of("sourceId", "accountId"))))) + .build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + +} diff --git a/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java b/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java index 1a083ed3522..41c38db12ad 100644 --- a/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java @@ -46,7 +46,7 @@ public class GothamAdsBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test-url.com/?pass={{AccountId}}"; + private static final String ENDPOINT_URL = "https://test-url.com/?pass={{AccountID}}"; private final GothamAdsBidder target = new GothamAdsBidder(ENDPOINT_URL, jacksonMapper); diff --git a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java index af166d87107..2595b6d903a 100644 --- a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java @@ -72,44 +72,6 @@ public void makeHttpRequestsShouldReturnErrorsIfImpExtCouldNotBeParsed() { assertThat(result.getValue()).isEmpty(); } - @Test - public void makeHttpRequestsShouldReturnErrorIfNoValidImpressions() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList( - givenImp(impBuilder -> impBuilder.video(Video.builder().build())))) - .build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(2) - .contains(BidderError.badInput("No valid impressions")); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfVideoFieldsAreNotValid() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .video(Video.builder().w(0).build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpGumgum.of("zone", BigInteger.TEN, "irisId", null, null)))) - .build())) - .build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()) - .containsExactlyInAnyOrder(BidderError.badInput("Invalid or missing video field(s)"), - BidderError.badInput("No valid impressions")); - assertThat(result.getValue()).isEmpty(); - } - @Test public void makeHttpRequestsShouldModifyVideoExtOfIrisIdIsPresent() { // given diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java index b75ea763ec6..82c9ff1e659 100644 --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java @@ -116,7 +116,7 @@ public void makeHttpRequestsShouldUseProperEndpoints() { public void makeHttpRequestsShouldProperProcessConsentedProvidersSetting() { // given final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90")) + .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90")) .build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder @@ -145,7 +145,7 @@ public void makeHttpRequestsShouldProperProcessConsentedProvidersSetting() { public void makeHttpRequestsShouldProperProcessConsentedProvidersSettingWithMultipleTilda() { // given final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90~anything")) + .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90~anything")) .build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder @@ -174,7 +174,7 @@ public void makeHttpRequestsShouldProperProcessConsentedProvidersSettingWithMult public void makeHttpRequestsShouldReturnUserExtIfConsentedProvidersIsNotProvided() { // given final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of(null)) + .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of(null)) .build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> @@ -192,28 +192,6 @@ public void makeHttpRequestsShouldReturnUserExtIfConsentedProvidersIsNotProvided .containsExactly(extUser); } - @Test - public void makeHttpRequestsShouldReturnErrorIfCannotParseConsentedProviders() { - // given - final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("1~a.fv.90")) - .build(); - - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder - .user(User.builder().ext(extUser).build()).id("request_id"), - identity()); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); - assertThat(error.getMessage()).startsWith("Cannot deserialize value of type"); - }); - } - @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given diff --git a/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java b/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java index c642ff5a2c6..e15093f49d4 100644 --- a/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java @@ -5,8 +5,6 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Native; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -24,10 +22,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.Function; +import java.util.function.UnaryOperator; import static java.util.Collections.singletonList; -import static java.util.function.Function.identity; +import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; @@ -152,7 +150,7 @@ public void makeHttpRequestsShouldUpdateOnlyFirstImpression() { @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given - final BidderCall httpCall = givenHttpCall(null, "invalid"); + final BidderCall httpCall = givenHttpCall("invalid"); // when final Result> result = target.makeBids(httpCall, null); @@ -167,8 +165,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { @Test public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(null)); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); // when final Result> result = target.makeBids(httpCall, null); @@ -181,8 +178,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces @Test public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(BidResponse.builder().build())); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); // when final Result> result = target.makeBids(httpCall, null); @@ -193,14 +189,9 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso } @Test - public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id(IMP_ID).banner(Banner.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(1))); // when final Result> result = target.makeBids(httpCall, null); @@ -208,17 +199,14 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), banner, null)); + .extracting(BidderBid::getType) + .containsExactly(banner); } @Test - public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(BidRequest.builder() - .imp(singletonList(Imp.builder().id(IMP_ID).video(Video.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(2))); // when final Result> result = target.makeBids(httpCall, null); @@ -226,18 +214,14 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws Js // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), video, null)); + .extracting(BidderBid::getType) + .containsExactly(video); } @Test - public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id(IMP_ID).xNative(Native.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(4))); // when final Result> result = target.makeBids(httpCall, null); @@ -245,37 +229,69 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), xNative, null)); + .extracting(BidderBid::getType) + .containsExactly(xNative); } - private static BidResponse givenBidResponse(Function bidCustomizer) { - return BidResponse.builder() - .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + @Test + public void makeBidsShouldReturnErrorOnInvalidBidType() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(identity())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Unsupported mtype null for bid bidId")); + } + + @Test + public void makeBidsShouldProceedWithErrorsAndValues() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(3))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Unsupported mtype 3 for bid bidId")); + + assertThat(result.getValue()).isEmpty(); + } + + private static String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder().id("bidId")).build())) .build())) - .build(); + .build()); } - private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + private static BidderCall givenHttpCall(String body) { return BidderCall.succeededHttp( - HttpRequest.builder().payload(bidRequest).build(), + HttpRequest.builder().build(), HttpResponse.of(200, null, body), null); } - private static BidRequest givenBidRequest(Function impCustomizer) { + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { return givenBidRequest(identity(), impCustomizer); } private static BidRequest givenBidRequest( - Function bidRequestCustomizer, - Function impCustomizer) { + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { return bidRequestCustomizer.apply(BidRequest.builder() .imp(singletonList(givenImp(impCustomizer)))) .build(); } - private static Imp givenImp(Function impCustomizer) { + private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id(IMP_ID) .banner(Banner.builder().id("bannerId").build()) diff --git a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java index 18f020f2bc0..35726f5a205 100644 --- a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java @@ -27,6 +27,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.ix.model.request.IxDiag; +import org.prebid.server.bidder.ix.model.response.AuctionConfigExtBidResponse; import org.prebid.server.bidder.ix.model.response.IxBidResponse; import org.prebid.server.bidder.ix.model.response.IxExtBidResponse; import org.prebid.server.bidder.ix.model.response.NativeV11Wrapper; @@ -49,7 +50,6 @@ import org.prebid.server.version.PrebidVersionProvider; import java.util.List; -import java.util.Map; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -778,7 +778,7 @@ public void makeBidderResponseShouldReturnFledgeAuctionConfig() throws JsonProce final IxBidResponse bidResponseWithFledge = IxBidResponse.builder() .cur(bidResponse.getCur()) .seatbid(bidResponse.getSeatbid()) - .ext(IxExtBidResponse.of(Map.of(impId, fledgeAuctionConfig))) + .ext(IxExtBidResponse.of(List.of(AuctionConfigExtBidResponse.of(impId, fledgeAuctionConfig)))) .build(); final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponseWithFledge)); diff --git a/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java b/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java new file mode 100644 index 00000000000..793b01f9333 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java @@ -0,0 +1,250 @@ +package org.prebid.server.bidder.loopme; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.loopme.ExtImpLoopme; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class LoopmeBidderTest extends VertxTest { + + public static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final LoopmeBidder loopmeBidder = new LoopmeBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy( + () -> new LoopmeBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Set.of("givenImp1", "givenImp2")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestForAllImps() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(Arrays.asList( + givenImp(identity()), + givenImp(identity())))); + + // when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(2); + } + + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest( + UnaryOperator impCustomizer, + UnaryOperator requestCustomizer) { + return requestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123")) + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpLoopme.of("somePublisherId", "someBundleId", "somePlacementId")))) + .build(); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp(HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java b/src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java new file mode 100644 index 00000000000..a62b8f0ab49 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java @@ -0,0 +1,424 @@ +package org.prebid.server.bidder.melozen; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.melozen.MeloZenImpExt; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +class MeloZenBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com/{{PublisherID}}"; + + @Mock + private CurrencyConversionService currencyConversionService; + + private MeloZenBidder target; + + @BeforeEach + public void before() { + target = new MeloZenBidder(currencyConversionService, ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + // when and then + assertThatIllegalArgumentException().isThrownBy(() -> + new MeloZenBidder(currencyConversionService, "invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(bidderError -> { + assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(bidderError.getMessage()).startsWith("Cannot deserialize value"); + }); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactlyInAnyOrderElementsOf(bidRequest.getImp()); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImpPerMediaType() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .xNative(Native.builder().build()), + imp -> imp.id("givenImp2") + .banner(null) + .xNative(null) + .video(Video.builder().build())); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(4) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactlyInAnyOrder( + givenImp(imp -> imp.id("givenImp1").banner(Banner.builder().build()).video(null).xNative(null)), + givenImp(imp -> imp.id("givenImp1").banner(null).video(Video.builder().build()).xNative(null)), + givenImp(imp -> imp.id("givenImp1").banner(null).video(null).xNative(Native.builder().build())), + givenImp(imp -> imp.id("givenImp2").banner(null).video(Video.builder().build()).xNative(null))); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpDoesNotHaveBannerVideoOrNative() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impid").banner(null).video(null).xNative(null)); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsOnly( + BidderError.badInput("Invalid MediaType. MeloZen only supports Banner, Video and Native.")); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(singleton("givenImp1"), singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("impId2").banner(null).video(null).xNative(null), + imp -> imp.id("impId3")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("impId3"); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test-url.com/publisherId"); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasUSDCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, "USD")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorAndAssignUSDCurrencyWhenBidfloorIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null).bidfloorcur("EUR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(null, "EUR")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasEmptyCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur(null)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, null)); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldConvertBidfloorToUSDWhenBidfloorHasAnotherCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "EUR", "USD")) + .willReturn(BigDecimal.ONE); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ONE, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseCanNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseDoesNotHaveSeatBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse()); + + // when + final Result> actual = target.makeBids(httpCall, null); + + // then + assertThat(actual.getValue()).isEmpty(); + assertThat(actual.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidBasedOnBidExt() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "banner"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid bannerBid = Bid.builder().impid("1").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, banner, null)); + + } + + @Test + public void makeBidsShouldReturnVideoBidBasedOnBidExt() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "video"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid videoBid = Bid.builder().impid("2").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, video, null)); + } + + @Test + public void makeBidsShouldReturnNativeBidOnBidExt() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "native"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid nativeBid = Bid.builder().impid("3").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(nativeBid, xNative, null)); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidExtCanNotResolveType() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "unknown"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid bid = Bid.builder().impid("3").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsOnly(BidderError.badServerResponse("Failed to parse bid mediatype for impression \"3\"")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(MeloZenBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .banner(Banner.builder().build()) + .bidfloor(BigDecimal.TEN) + .bidfloorcur("USD") + .ext(mapper.valueToTree(ExtPrebid.of(null, MeloZenImpExt.of("publisherId"))))) + .build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(asList(bids)).build())) + .build()); + } + + private static Bid givenBid(UnaryOperator bidCustomizer) { + return bidCustomizer.apply(Bid.builder()).build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java b/src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java new file mode 100644 index 00000000000..605b56315da --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java @@ -0,0 +1,371 @@ +package org.prebid.server.bidder.metax; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.metax.ExtImpMetax; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class MetaxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com?sid={{publisherId}}&adunit={{adUnit}}"; + + private MetaxBidder target; + + @BeforeEach + public void setUp() { + target = new MetaxBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy( + () -> new MetaxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?sid=123&adunit=456"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final Imp givenInvalidImp = givenImp(imp -> imp + .id("impIdCorrupted") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final Imp givenValidImp = givenImp(identity()); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(givenInvalidImp, givenValidImp)) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("123"); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(identity()), givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldNotChangeBannerWidthAndHeightIfPresent() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .banner(Banner.builder() + .w(12) + .h(34) + .format(singletonList(Format.builder().w(56).h(78).build())) + .build()) + ); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBanner) + .extracting(Banner::getW, Banner::getH) + .containsExactly(Tuple.tuple(12, 34)); + } + + @Test + public void makeHttpRequestsShouldChangeBannerWidthAndHeightIfPresentInFormats() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .banner(Banner.builder() + .w(null) + .h(null) + .format(singletonList(Format.builder().w(56).h(78).build())) + .build()) + ); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBanner) + .extracting(Banner::getW, Banner::getH) + .containsExactly(Tuple.tuple(56, 78)); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(3).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(3).impid("123").build(), audio, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsNotSupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(6).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Unsupported MType: 123"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMetax.of(123, 456))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java new file mode 100644 index 00000000000..83326e4049f --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java @@ -0,0 +1,274 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singleton; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.prebid.server.util.HttpUtil.REFERER_HEADER; +import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER; +import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +class MissenaBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com"; + + private final MissenaBidder target = new MissenaBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Error parsing missenaExt parameters")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("apiKey1", "plId1", "test1")), + imp -> imp.id("givenImp2").ext(givenImpExt("apiKey2", "plId2", "test2"))) + .toBuilder() + .id("requestId") + .site(Site.builder().page("page").domain("domain").build()) + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + final MissenaAdRequest expectedRequest = MissenaAdRequest.builder() + .requestId("requestId") + .timeout(2000) + .referer("page") + .refererCanonical("domain") + .consentString("consent") + .consentRequired(true) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .containsExactlyInAnyOrder( + expectedRequest.toBuilder().placement("plId1").test("test1").build(), + expectedRequest.toBuilder().placement("plId2").test("test2").build()); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(singleton("givenImp1"), singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIp() { + // given + final BidRequest bidRequest = givenBidRequest(identity()) + .toBuilder() + .site(Site.builder().page("page").build()) + .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)) + .isEqualTo("ua")) + .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)) + .isEqualTo("ip")) + .satisfies(headers -> assertThat(headers.get(REFERER_HEADER)) + .isEqualTo("page")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpv6Only() { + // given + final BidRequest bidRequest = givenBidRequest(identity()) + .toBuilder() + .device(Device.builder().ip(null).ipv6("ipv6").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)).isNull()) + .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)).isEqualTo("ipv6")) + .satisfies(headers -> assertThat(headers.get(REFERER_HEADER)).isNull()); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("impId2").ext(givenImpExt("apiKey", "placement", "testMode"))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final MissenaAdRequest expectedRequest = MissenaAdRequest.builder() + .timeout(2000) + .consentString("") + .consentRequired(false) + .test("testMode") + .placement("placement") + .build(); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsOnly(expectedRequest); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("apiKey", "plId", "test"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test-url.com?t=apiKey"); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1"), imp -> imp.id("impId2")) + .toBuilder().id("requestId").build(); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode"); + }); + } + + @Test + public void makeBidsShouldReturnSingleBid() throws JsonProcessingException { + // given + final MissenaAdResponse bidResponse = MissenaAdResponse.builder() + .requestId("id") + .cpm(BigDecimal.TEN) + .currency("USD") + .ad("adm") + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1"), imp -> imp.id("impId2")) + .toBuilder().id("requestId").build(); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final Bid expetedBid = Bid.builder() + .adm("adm") + .price(BigDecimal.TEN) + .crid("id") + .impid("impId1") + .id("requestId") + .build(); + + assertThat(result.getValue()).hasSize(1) + .containsOnly(BidderBid.of(expetedBid, BidType.banner, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(MissenaBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(givenImpExt("apikey", "placementId", "test"))) + .build(); + } + + private static ObjectNode givenImpExt(String apiKey, String placement, String testMode) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpMissena.of(apiKey, placement, testMode))); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java index 9ad08ecb30a..6de8d3f4d65 100644 --- a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java @@ -221,7 +221,6 @@ public void makeHttpRequestsShouldModifyImpWithAddingSkadnWhenSkadnIsPresent() { final Result>> result = target.makeHttpRequests(bidRequest); // then - final ObjectNode expectedImpExt = mapper.createObjectNode().set("skadn", skadn); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) diff --git a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java index 315f0499cee..1f218984f91 100644 --- a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java @@ -34,6 +34,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.openx.ExtImpOpenx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.math.BigDecimal; @@ -522,6 +523,42 @@ public void makeBidsShouldReturnResultWithExpectedFields() throws JsonProcessing .build()); } + @Test + public void makeBidsShouldReturnVideoInfoWhenAvailable() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder() + .w(200) + .h(150) + .price(BigDecimal.ONE) + .impid("impId1") + .dealid("dealid") + .adm("
This is an Ad
") + .dur(30) + .cat(singletonList("category1")) + .build())) + .build())) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .build())) + .build(); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1) + .extracting(BidderBid::getVideoInfo) + .containsExactly(ExtBidPrebidVideo.of(30, "category1")); + } + @Test public void makeBidsShouldReturnFledgeConfigEvenIfNoBids() throws JsonProcessingException { // given diff --git a/src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java b/src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java new file mode 100644 index 00000000000..987d71658c2 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java @@ -0,0 +1,327 @@ +package org.prebid.server.bidder.oraki; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder.OrakiImpExtBidderBuilder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.oraki.ExtImpOraki; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class OrakiBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final OrakiBidder target = new OrakiBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new OrakiBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final Imp givenInvalidImp = givenImp(imp -> imp + .id("impIdCorrupted") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final Imp givenValidImp = givenImp(identity()); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(givenInvalidImp, givenValidImp)) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("123"); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(identity()), givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOraki.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtOrakiBidder(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOraki.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtOrakiBidder(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOraki.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExtOrakiBidder(UnaryOperator impExtOraki) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExtOraki.apply(OrakiImpExtBidder.builder()).build(), + JsonNode.class)); + } +} + diff --git a/src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java b/src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java new file mode 100644 index 00000000000..ac520ff3883 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java @@ -0,0 +1,345 @@ +package org.prebid.server.bidder.ownadx; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ownadx.ExtImpOwnAdx; + +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +public class OwnAdxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.com/test/{{SeatID}}/{{SspID}}?token={{TokenID}}"; + + private final OwnAdxBidder target = new OwnAdxBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new OwnAdxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldPassImpIdToHttpRequestImpIdsAndCorrectlyPopulateBidRequest() { + // given + final String firstId = "234"; + final String secondId = "123"; + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of( + givenImp(impBuilder -> impBuilder.id(firstId)), + givenImp(impBuilder -> impBuilder.id(secondId)))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .flatExtracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(firstId, secondId, firstId, secondId); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .containsExactlyInAnyOrder(bidRequest, bidRequest); + } + + @Test + public void makeHttpRequestsShouldReturnOneValidAndInvalidInHttpRequest() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of( + givenImp(impBuilder -> impBuilder + .id("321") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))), + givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1); + assertThat(result.getErrors()) + .hasSize(1) + .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 321")); + } + + @Test + public void makeHttpRequestsShouldCorrectlyAddHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .flatExtracting(res -> res.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple("Content-Type", "application/json;charset=utf-8"), + tuple("Accept", "application/json"), + tuple("x-openrtb-version", "2.5")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 123")); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(500).build())) + .build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.com/test/seatId/sspId?token=token"); + } + + @Test + public void makeHttpRequestsShouldNotReplaceMacrosWhenInExtOwnAdxAllFieldAreNull() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOwnAdx.of(null, null, null))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.com/test//?token="); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfMtypeIsOne() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(1)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfMtypeIsTwo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(2)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidIfMtypeIsThree() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(3)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfMtypeIsFour() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(4)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenTypeContainUnknownValue() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(10)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .satisfies(error -> { + assertThat(error).extracting(BidderError::getType) + .containsExactly(BidderError.Type.bad_server_response); + assertThat(error).extracting(BidderError::getMessage) + .containsExactly("Unable to fetch mediaType 10"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenTypeIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.id("123").mtype(null)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .satisfies(error -> { + assertThat(error).extracting(BidderError::getType) + .containsExactly(BidderError.Type.bad_server_response); + assertThat(error).extracting(BidderError::getMessage) + .containsExactly("Missing MType for bid: 123"); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(UnaryOperator.identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOwnAdx.of("sspId", "seatId", "token"))))) + .build(); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java b/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java index e2b8c24ebd2..c349fd62cbe 100644 --- a/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java @@ -8,16 +8,23 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.pgamssp.PgamSspImpExt; +import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -27,6 +34,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.prebid.server.bidder.model.BidderError.badInput; import static org.prebid.server.bidder.model.BidderError.badServerResponse; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; @@ -37,15 +47,46 @@ import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; +@ExtendWith(MockitoExtension.class) public class PgamSspBidderTest extends VertxTest { private static final String ENDPOINT_URL = "http://test-url.com"; - private final PgamSspBidder target = new PgamSspBidder(ENDPOINT_URL, jacksonMapper); + @Mock + private CurrencyConversionService currencyConversionService; + + private PgamSspBidder target; + + @BeforeEach + public void setUp() { + target = new PgamSspBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new PgamSspBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> + new PgamSspBidder("invalid_url", currencyConversionService, jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBidderCurrency() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(BDDAssertions.tuple(BigDecimal.TEN, "USD")); } @Test diff --git a/src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java b/src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java new file mode 100644 index 00000000000..82d33a0b838 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java @@ -0,0 +1,333 @@ +package org.prebid.server.bidder.pubrise; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.pubrise.proto.PubriseImpExtBidder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.pubrise.ExtImpPubrise; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class PubriseBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final PubriseBidder target = new PubriseBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new PubriseBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("validImpId")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("validImpId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenNoValidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsOnly(BidderError.badInput("found no valid impressions")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpPubrise.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpPubrise.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("impId").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("impId").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(PubriseBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpPubrise.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExt(UnaryOperator impExt) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExt.apply(PubriseImpExtBidder.builder()).build(), + JsonNode.class)); + } +} diff --git a/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java b/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java new file mode 100644 index 00000000000..71daee260a6 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java @@ -0,0 +1,326 @@ +package org.prebid.server.bidder.qt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder.QtImpExtBidderBuilder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.qt.ExtImpQt; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class QtBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final QtBidder target = new QtBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new QtBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final Imp givenInvalidImp = givenImp(imp -> imp + .id("impIdCorrupted") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final Imp givenValidImp = givenImp(identity()); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(givenInvalidImp, givenValidImp)) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("123"); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(identity()), givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpQt.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtQtBidder(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpQt.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtQtBidder(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpQt.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExtQtBidder(UnaryOperator impExtQt) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExtQt.apply(QtImpExtBidder.builder()).build(), + JsonNode.class)); + } +} diff --git a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java index a31a943c0e2..e6429a971d1 100644 --- a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java @@ -184,7 +184,6 @@ public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBi @Test public void makeHttpRequestsShouldTakePriceFloorsWhenBidfloorParamIsAlsoPresent() { // given - final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .bidfloor(BigDecimal.TEN).bidfloorcur("USD") @@ -209,7 +208,6 @@ public void makeHttpRequestsShouldTakePriceFloorsWhenBidfloorParamIsAlsoPresent( @Test public void makeHttpRequestsShouldTakeBidfloorExtImpParamIfNoBidfloorInRequest() { // given - final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, @@ -301,6 +299,28 @@ public void makeBidsShouldParseNativeAdmData() throws JsonProcessingException { .containsExactly("{\"property1\":\"value1\"}"); } + @Test + public void makeBidsShouldReturnBidWithResolvedMacros() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(null)); + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(givenBidResponse( + bidBuilder -> bidBuilder + .nurl("nurl:${AUCTION_PRICE}") + .adm("adm:${AUCTION_PRICE}") + .price(BigDecimal.valueOf(12.34))))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsExactly(tuple("nurl:12.34", "adm:12.34")); + } + private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() .cur("USD") diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index 98f3d12347a..d49f874c0e8 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -108,6 +108,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.io.IOException; import java.math.BigDecimal; @@ -147,8 +148,10 @@ public class RubiconBidderTest extends VertxTest { private static final String BIDDER_NAME = "bidderName"; private static final String ENDPOINT_URL = "http://rubiconproject.com/exchange.json?tk_xint=prebid"; + private static final String EXTERNAL_URL = "http://localhost:8080"; private static final String USERNAME = "username"; private static final String PASSWORD = "password"; + private static final String PBS_VERSION = "pbs_version"; private static final List SUPPORTED_VENDORS = Arrays.asList("activeview", "comscore", "doubleverify", "integralads", "moat", "sizmek", "whiteops"); @@ -158,20 +161,27 @@ public class RubiconBidderTest extends VertxTest { @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; + @Mock(strictness = LENIENT) + private PrebidVersionProvider versionProvider; + private RubiconBidder target; @BeforeEach public void setUp() { - target = new RubiconBidder(BIDDER_NAME, + target = new RubiconBidder( + BIDDER_NAME, ENDPOINT_URL, + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, false, - true, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper); + + given(versionProvider.getNameVersionRecord()).willReturn("pbs_version"); } @Test @@ -179,13 +189,14 @@ public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy( () -> new RubiconBidder(BIDDER_NAME, "invalid_url", + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, false, - true, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper)); } @@ -623,17 +634,17 @@ public void makeHttpRequestsShouldFillImpExt() { final Result>> result = target.makeHttpRequests(bidRequest); // then + final ObjectNode expectedTarget = givenImpExtRpTarget().setAll( + (ObjectNode) mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech")))); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1).doesNotContainNull() .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).doesNotContainNull() .extracting(Imp::getExt).doesNotContainNull() .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)) - .containsOnly(RubiconImpExt.builder() - .rp(RubiconImpExtRp.of(4001, - mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))), - RubiconImpExtRpTrack.of("", ""), - null)) + .containsExactly(RubiconImpExt.builder() + .rp(RubiconImpExtRp.of(4001, expectedTarget, RubiconImpExtRpTrack.of("", ""), null)) .skadn(givenSkadn) .maxbids(1) .build()); @@ -776,6 +787,31 @@ public void makeHttpRequestsShouldResolveImpBidFloorCurrencyIfNotUSDAndCallCurre .containsOnly(tuple(BigDecimal.TEN, "USD")); } + @Test + public void makeHttpRequestsShouldResolveImpBidFloorCurrencyIfNotUSDAndBidFloorIsZero() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder + .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build()) + .bidfloor(BigDecimal.ZERO).bidfloorcur("EUR"), + identity()); + + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.ZERO); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + verify(currencyConversionService).convertCurrency(eq(BigDecimal.ZERO), any(), eq("EUR"), eq("USD")); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp).doesNotContainNull() + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ZERO, "USD")); + } + @Test public void makeHttpRequestsShouldNotSetBidFloorCurrencyToUSDIfNull() { // given @@ -827,18 +863,19 @@ public void makeHttpRequestsShouldIgnoreBidRequestIfCurrencyServiceThrowsAnExcep } @Test - public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsIsMissingSizeId() { + public void shouldNotSetSizeIfBidderParamsIsMissingSizeId() { // given target = new RubiconBidder( BIDDER_NAME, ENDPOINT_URL, + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, - false, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper); final BidRequest bidRequest = givenBidRequest( builder -> builder.instl(1).video(Video.builder().placement(1).build()), @@ -857,109 +894,6 @@ public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsI .containsOnlyNulls(); } - @Test - public void shouldSetSizeFromBidderParamsWhenVideoSizeProcessingLogicIsDisabled() { - // given - target = new RubiconBidder( - BIDDER_NAME, - ENDPOINT_URL, - USERNAME, - PASSWORD, - SUPPORTED_VENDORS, - true, - false, - currencyConversionService, - priceFloorResolver, - jacksonMapper); - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(1).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(14).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(14); - } - - @Test - public void shouldSetSizeIdTo201IfPlacementIs1AndSizeIdIsNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(1).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(201); - } - - @Test - public void shouldSetSizeIdTo203IfPlacementIs3AndSizeIdIsNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(3).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(203); - } - - @Test - public void shouldSetSizeIdTo202UsingInstlFlagIfPlacementAndSizeIdAreNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(null).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(202); - } - @Test public void makeHttpRequestsShouldFillVideoExt() { // given @@ -2028,10 +1962,10 @@ public void makeHttpRequestsShouldNotCreateUserIfVisitorAndConsentNotPresent() { public void makeHttpRequestsShouldUseGivenUserIdIfOtherExtUserFieldsPassed() { // given final ExtUser extUser = ExtUser.builder() - .eids(singletonList(Eid.of( - "liveramp.com", - singletonList(Uid.of("firstId", null, null)), - null))) + .eids(singletonList(Eid.builder() + .source("liveramp.com") + .uids(singletonList(Uid.builder().id("firstId").build())) + .build())) .build(); final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .id("userId") @@ -2055,13 +1989,21 @@ public void makeHttpRequestsShouldUseGivenUserIdIfOtherExtUserFieldsPassed() { public void makeHttpRequestsShouldCreateUserIdIfMissingFromFirstUidStypePpuid() { // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() - .eids(singletonList(Eid.of( - null, - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.valueToTree(Map.of("stype", "ppuid"))), - Uid.of("id3", null, mapper.valueToTree(Map.of("stype", "ppuid")))), - null))) + .eids(singletonList(Eid.builder() + .uids(asList( + Uid.builder() + .id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build(), + Uid.builder() + .id("id2") + .ext(mapper.valueToTree(Map.of("stype", "ppuid"))) + .build(), + Uid.builder() + .id("id3") + .ext(mapper.valueToTree(Map.of("stype", "ppuid"))) + .build())) + .build())) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2081,12 +2023,16 @@ public void makeHttpRequestsShouldNotCreateUserIdIfMissingWhenNoUidWithPpuidType // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - null, - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.valueToTree(Map.of("stype", "other")))), - null))) + .eids(singletonList(Eid.builder().uids(asList( + Uid.builder() + .id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build(), + Uid.builder() + .id("id2") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2104,15 +2050,28 @@ public void makeHttpRequestsShouldNotCreateUserIdIfMissingWhenNoUidWithPpuidType @Test public void makeHttpRequestsShouldRemoveStypesPpuidSha256emailDmp() { // given - final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() - .eids(singletonList(Eid.of( - "source", - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.valueToTree(Map.of("stype", "ppuid"))), - Uid.of("id3", null, mapper.valueToTree(Map.of("stype", "sha256email"))), - Uid.of("id4", null, mapper.valueToTree(Map.of("stype", "dmp")))), - null))) + final BidRequest bidRequest = givenBidRequest( + builder -> builder.user(User.builder() + .eids(singletonList(Eid.builder() + .source("source") + .uids(asList( + Uid.builder() + .id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build(), + Uid.builder() + .id("id2") + .ext(mapper.valueToTree(Map.of("stype", "ppuid"))) + .build(), + Uid.builder() + .id("id3") + .ext(mapper.valueToTree(Map.of("stype", "sha256email"))) + .build(), + Uid.builder() + .id("id4") + .ext(mapper.valueToTree(Map.of("stype", "dmp"))) + .build())) + .build())) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2125,14 +2084,15 @@ public void makeHttpRequestsShouldRemoveStypesPpuidSha256emailDmp() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(request -> request.getUser().getExt()).hasSize(1).element(0) .isEqualTo(ExtUser.builder() - .eids(singletonList(Eid.of( - "source", - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.createObjectNode()), - Uid.of("id3", null, mapper.createObjectNode()), - Uid.of("id4", null, mapper.createObjectNode())), - null))) + .eids(singletonList(Eid.builder() + .source("source") + .uids(asList( + Uid.builder().id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))).build(), + Uid.builder().id("id2").ext(mapper.createObjectNode()).build(), + Uid.builder().id("id3").ext(mapper.createObjectNode()).build(), + Uid.builder().id("id4").ext(mapper.createObjectNode()).build())) + .build())) .build()); } @@ -2141,10 +2101,10 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithAdServerEidSourceIfExt // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of("id", null, null)), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder().id("id").build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2158,10 +2118,10 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithAdServerEidSourceIfExt .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(request -> request.getUser().getExt()) .containsOnly(ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of("id", null, null)), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder().id("id").build())) + .build())) .build()); } @@ -2170,13 +2130,13 @@ public void makeHttpRequestsShouldCreateUserExtEidsWithAdServerEidSource() { // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of( - "adServerUid", - null, - mapper.valueToTree(Map.of("rtiPartner", "TDID")))), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder() + .id("adServerUid") + .ext(mapper.valueToTree(Map.of("rtiPartner", "TDID"))) + .build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2191,13 +2151,13 @@ public void makeHttpRequestsShouldCreateUserExtEidsWithAdServerEidSource() { .extracting(request -> request.getUser().getExt()) .containsOnly(jacksonMapper.fillExtension( ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of( - "adServerUid", - null, - mapper.valueToTree(Map.of("rtiPartner", "TDID")))), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder() + .id("adServerUid") + .ext(mapper.valueToTree(Map.of("rtiPartner", "TDID"))) + .build())) + .build())) .build(), RubiconUserExt.builder().build())); } @@ -2207,8 +2167,8 @@ public void makeHttpRequestsShouldIgnoreLiverampIdIfMissingEidUidId() { // given final ExtUser extUser = ExtUser.builder() .eids(asList( - Eid.of("liveramp.com", null, null), - Eid.of("liveramp.com", emptyList(), null))) + Eid.builder().source("liveramp.com").build(), + Eid.builder().source("liveramp.com").uids(emptyList()).build())) .build(); final BidRequest bidRequest = givenBidRequest( builder -> builder.user(User.builder().ext(extUser).build()), @@ -2231,13 +2191,13 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithUnknownEidSource() { // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - "unknownSource", - singletonList(Uid.of( - "id", - null, - mapper.valueToTree(Map.of("rtiPartner", "eidUidId")))), - null))) + .eids(singletonList(Eid.builder() + .source("unknownSource") + .uids(singletonList(Uid.builder() + .id("id") + .ext(mapper.valueToTree(Map.of("rtiPartner", "eidUidId"))) + .build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2251,10 +2211,13 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithUnknownEidSource() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(request -> request.getUser().getExt()) .containsOnly(ExtUser.builder() - .eids(singletonList(Eid.of( - "unknownSource", - singletonList(Uid.of("id", null, mapper.valueToTree(Map.of("rtiPartner", "eidUidId")))), - null))) + .eids(singletonList(Eid.builder() + .source("unknownSource") + .uids(singletonList(Uid.builder() + .id("id") + .ext(mapper.valueToTree(Map.of("rtiPartner", "eidUidId"))) + .build())) + .build())) .build()); } @@ -2493,11 +2456,14 @@ public void makeHttpRequestsShouldCreateRequestPerImp() { final Result>> result = target.makeHttpRequests(bidRequest); // then + final RubiconImpExtRp expectedImpExtRp = RubiconImpExtRp.of( + null, givenImpExtRpTarget(), RubiconImpExtRpTrack.of("", ""), null); + final BidRequest expectedBidRequest1 = BidRequest.builder() .imp(singletonList(Imp.builder() .video(Video.builder().build()) .ext(mapper.valueToTree(RubiconImpExt.builder() - .rp(RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", ""), null)) + .rp(expectedImpExtRp) .maxbids(1) .build())) .build())) @@ -2508,7 +2474,7 @@ public void makeHttpRequestsShouldCreateRequestPerImp() { .video(Video.builder().build()) .ext(mapper.valueToTree( RubiconImpExt.builder() - .rp(RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", ""), null)) + .rp(expectedImpExtRp) .maxbids(1) .build())) .build())) @@ -2544,8 +2510,7 @@ public void makeHttpRequestsShouldCopyAndModifyDataFieldsToRubiconImpExtRpTarget .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode() - .set("property2", mapper.createArrayNode().add("value2"))); + .containsExactly(givenImpExtRpTarget().set("property2", mapper.createArrayNode().add("value2"))); } @Test @@ -2592,7 +2557,7 @@ public void makeHttpRequestsShouldCopySiteExtDataFieldsToRubiconImpExtRpTarget() .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode().set("property", mapper.createArrayNode().add("value"))); + .containsExactly(givenImpExtRpTarget().set("property", mapper.createArrayNode().add("value"))); } @Test @@ -2618,7 +2583,27 @@ public void makeHttpRequestsShouldCopyAppExtDataFieldsToRubiconImpExtRpTarget() .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode().set("property", mapper.createArrayNode().add("value"))); + .containsOnly(givenImpExtRpTarget().set("property", mapper.createArrayNode().add("value"))); + } + + @Test + public void makeHttpRequestsShouldSetXapiFieldsToRubiconImpExtRpTarget() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.video(Video.builder().build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) + .extracting(RubiconImpExt::getRp) + .extracting(RubiconImpExtRp::getTarget) + .containsExactly(givenImpExtRpTarget()); } @Test @@ -2737,6 +2722,9 @@ public void makeHttpRequestsShouldCopyDataSearchToRubiconImpExtRpTargetSearch() final Result>> result = target.makeHttpRequests(bidRequest); // then + final ObjectNode expectedTarget = givenImpExtRpTarget() + .set("search", mapper.createArrayNode().add("imp ext data search")); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) @@ -2745,7 +2733,7 @@ public void makeHttpRequestsShouldCopyDataSearchToRubiconImpExtRpTargetSearch() .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.readTree("{\"search\":[\"imp ext data search\"]}")); + .containsExactly(expectedTarget); } @Test @@ -2794,8 +2782,7 @@ public void makeHttpRequestsShouldMergeSiteAttributesAndCopyToRubiconImpExtRpTar .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode() - .set("page", mapper.createArrayNode().add("site page"))); + .containsExactly(givenImpExtRpTarget().set("page", mapper.createArrayNode().add("site page"))); } @Test @@ -3002,8 +2989,8 @@ public void makeHttpRequestsShouldReturnOnlyLineItemRequestsWithExpectedFieldsWh .flatExtracting(BidRequest::getImp) .extracting(imp -> mapper.treeToValue(imp.getExt(), RubiconImpExt.class).getRp().getTarget()) .containsOnly( - mapper.readTree("{\"line_item\":\"123\"}"), - mapper.readTree("{\"line_item\":\"234\"}")); + givenImpExtRpTarget().put("line_item", "123"), + givenImpExtRpTarget().put("line_item", "234")); } @Test @@ -3318,6 +3305,34 @@ public void makeBidsShouldReplaceNotPresentAdmWithAdmNative() throws JsonProcess .containsExactly("{\"admNativeProperty\":\"admNativeValue\"}"); } + @Test + public void makeBidsShouldSetSeatToMetaSeat() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + mapper.writeValueAsString(RubiconBidResponse.builder() + .cur("USD") + .seatbid(singletonList(RubiconSeatBid.builder() + .seat("seat") + .bid(singletonList(givenRubiconBid(bid -> bid.price(ONE)))) + .build())) + .build())); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + final ObjectNode expectedBidExt = mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().seat("seat").build()) + .build(), null)); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly(expectedBidExt); + } + @Test public void makeBidsShouldSetSeatBuyerToMetaNetworkId() throws JsonProcessingException { // given @@ -3719,8 +3734,8 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessingException { // given target = new RubiconBidder( - BIDDER_NAME, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, - currencyConversionService, priceFloorResolver, jacksonMapper); + BIDDER_NAME, ENDPOINT_URL, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, + currencyConversionService, priceFloorResolver, versionProvider, jacksonMapper); final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), mapper.writeValueAsString(RubiconBidResponse.builder() @@ -3745,8 +3760,8 @@ public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessi public void makeBidsShouldReturnBidWithCurrencyFromBidResponse() throws JsonProcessingException { // given target = new RubiconBidder( - BIDDER_NAME, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, - currencyConversionService, priceFloorResolver, jacksonMapper); + BIDDER_NAME, ENDPOINT_URL, EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, + currencyConversionService, priceFloorResolver, versionProvider, jacksonMapper); final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), mapper.writeValueAsString(RubiconBidResponse.builder() @@ -3767,13 +3782,17 @@ public void makeBidsShouldReturnBidWithCurrencyFromBidResponse() throws JsonProc } @Test - public void makeBidsShouldReturnBidWithDchainFromRequest() throws JsonProcessingException { + public void makeBidsShouldReturnBidMetaFromRequest() throws JsonProcessingException { // given final ObjectNode requestNode = mapper.valueToTree(ExtBidPrebid.builder() .meta(ExtBidPrebidMeta.builder() .dchain(mapper.createObjectNode().set("dChain", TextNode.valueOf("dChain"))) + .mediaType("banner") .build()) .build()); + + final ObjectNode expectedNode = requestNode.deepCopy(); + final BidderCall httpCall = givenHttpCall( givenBidRequest(identity()), mapper.writeValueAsString(RubiconBidResponse.builder() @@ -3790,7 +3809,7 @@ public void makeBidsShouldReturnBidWithDchainFromRequest() throws JsonProcessing assertThat(result.getValue()) .extracting(BidderBid::getBid) .extracting(Bid::getExt) - .containsExactly(requestNode); + .containsExactly(expectedNode); } @Test @@ -3942,6 +3961,13 @@ private static Data givenTestDataWithSegmentEntries(Integer segtax) { .build(); } + private static ObjectNode givenImpExtRpTarget() { + return mapper.createObjectNode() + .put("pbs_login", USERNAME) + .put("pbs_version", PBS_VERSION) + .put("pbs_url", EXTERNAL_URL); + } + @AllArgsConstructor(staticName = "of") @Value private static class Inventory { diff --git a/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java b/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java index 2197850e023..f734090592c 100644 --- a/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java @@ -79,8 +79,10 @@ public void makeHttpRequestsShouldFailOnMissingPublisherId() { @Test public void makeHttpRequestsShouldPassEidsFromDataToExtEids() { // given - final List givenEids = - List.of(Eid.of("testSource", List.of(Uid.of("testUidId", 2, null)), null)); + final List givenEids = List.of(Eid.builder() + .source("testSource") + .uids(List.of(Uid.builder().id("testUidId").atype(2).build())) + .build()); final ObjectNode givenDataNode = mapper.createObjectNode(); givenDataNode.set("eids", mapper.valueToTree(givenEids)); final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder diff --git a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java index 24531719f48..18b1ba36e8a 100644 --- a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java @@ -921,7 +921,6 @@ public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsNative() throws JsonProc final Result> result = target.makeBids(httpCall, null); // then - final String expectedAdm = "{\"assets\":[{\"id\":1,\"img\":{\"type\":3," + "\"url\":\"https://smaato.com/image.png\",\"w\":480,\"h\":320}}]," + "\"link\":{\"url\":\"https://www.smaato.com\"}}"; diff --git a/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java b/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java index 11bac67cb40..d5152ee00cc 100644 --- a/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java @@ -8,7 +8,11 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -16,9 +20,11 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.sonobi.ExtImpSonobi; +import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -27,18 +33,31 @@ import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verifyNoInteractions; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +@ExtendWith(MockitoExtension.class) public class SonobiBidderTest extends VertxTest { public static final String ENDPOINT_URL = "https://test.endpoint.com"; - private final SonobiBidder target = new SonobiBidder(ENDPOINT_URL, jacksonMapper); + @Mock + private CurrencyConversionService currencyConversionService; + + private SonobiBidder target; + + @BeforeEach + public void before() { + target = new SonobiBidder(currencyConversionService, ENDPOINT_URL, jacksonMapper); + } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new SonobiBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> + new SonobiBidder(currencyConversionService, "invalid_url", jacksonMapper)); } @Test @@ -60,7 +79,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldReturnExpectedBidRequest() { + public void makeHttpRequestsShouldReturnImpWithSetTagId() { // given final BidRequest bidRequest = givenBidRequest(identity()); @@ -68,14 +87,90 @@ public void makeHttpRequestsShouldReturnExpectedBidRequest() { final Result>> result = target.makeHttpRequests(bidRequest); // then - final BidRequest expectedRequest = bidRequest.toBuilder() - .imp(singletonList(bidRequest.getImp().getFirst().toBuilder() - .tagid("tagidString").build())) - .build(); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .containsOnly(expectedRequest); + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsOnly("tagidString"); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasUSDCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, "USD")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasInvalidPrice() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.ZERO).bidfloorcur("GBR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ZERO, "GBR")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasEmptyCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur(null)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, null)); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldConvertBidfloorToUSDWhenBidfloorHasAnotherCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "EUR", "USD")) + .willReturn(BigDecimal.ONE); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ONE, "USD")); + } @Test @@ -204,7 +299,7 @@ public void makeBidsShouldReturnErrorsForBidsThatDoesNotMatchImp() throws JsonPr final Result> result = target.makeBids(httpCall, null); // then - assertThat(result.getValue()).containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .extracting(BidderError::getMessage) .containsExactly("Failed to find impression for ID: 456"); @@ -214,7 +309,10 @@ private static BidRequest givenBidRequest( Function impCustomizer, Function requestCustomizer) { - return requestCustomizer.apply(BidRequest.builder().imp(singletonList(givenImp(impCustomizer)))).build(); + return requestCustomizer.apply(BidRequest.builder() + .cur(singletonList("USD")) + .imp(singletonList(givenImp(impCustomizer)))) + .build(); } private static BidRequest givenBidRequest(Function impCustomizer) { diff --git a/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java b/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java index 4406373d114..a185687bb5f 100644 --- a/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -337,6 +338,7 @@ public void makeHttpRequestsShouldModifyExtIfImpExtPageTypeIsNotEmpty() { public void makeHttpRequestShouldContainProperUriWhenTypeIsBanner() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().build()), givenBannerImp(identity(), ext -> ext.publisherId("publisherId"))); // when @@ -353,6 +355,7 @@ public void makeHttpRequestShouldContainProperUriWhenTypeIsBanner() { public void makeHttpRequestShouldContainProperUriWithEncodedPublisherId() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().build()), givenBannerImp(identity(), extImp -> extImp.publisherId("not/encoded"))); // when @@ -369,6 +372,7 @@ public void makeHttpRequestShouldContainProperUriWithEncodedPublisherId() { public void makeHttpRequestShouldContainProperUriWhenTypeIsNative() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().build()), givenImp(imp -> imp.xNative(Native.builder().build()), ext -> ext.publisherId("publisherId"))); // when @@ -411,30 +415,38 @@ public void makeHttpRequestShouldModifySiteDependsOnExtPublisherId() { } @Test - public void makeHttpRequestShouldModifySiteDomainIfExtPublisherDomainIsNotEmpty() { + public void makeHttpRequestShouldModifyAppDependsOnExtPublisherId() { // given + final App givenApp = App.builder() + .id("id") + .publisher(Publisher.builder().id("id").build()) + .build(); + final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().domain("domain").build()), - givenBannerImp(identity(), ext -> ext.publisherDomain("extDomain"))); + request -> request.app(givenApp), + givenBannerImp(identity(), ext -> ext.publisherId("extPublisherId"))); // when final Result>> result = target.makeHttpRequests(bidRequest); // then + final App expectedApp = givenApp.toBuilder() + .publisher(Publisher.builder().id("extPublisherId").build()) + .id("extPublisherId") + .build(); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .extracting(Site::getDomain) - .containsExactly("extDomain"); + .extracting(BidRequest::getApp) + .containsOnly(expectedApp); } @Test - public void makeHttpRequestShouldNotModifySiteDomainIfExtPublisherDomainIsEmpty() { + public void makeHttpRequestShouldModifySiteDomainIfExtPublisherDomainIsNotEmpty() { // given final BidRequest bidRequest = givenBidRequest( request -> request.site(Site.builder().domain("domain").build()), - givenBannerImp(identity(), ext -> ext.publisherDomain(""))); + givenBannerImp(identity(), ext -> ext.publisherDomain("extDomain"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -445,15 +457,15 @@ public void makeHttpRequestShouldNotModifySiteDomainIfExtPublisherDomainIsEmpty( .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) .extracting(Site::getDomain) - .containsExactly("domain"); + .containsExactly("extDomain"); } @Test - public void makeHttpRequestShouldAddEmptyDomainIfNoOtherSources() { + public void makeHttpRequestShouldNotModifySiteDomainIfExtPublisherDomainIsEmpty() { // given final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().build()), - givenBannerImp(identity(), identity())); + request -> request.site(Site.builder().domain("domain").build()), + givenBannerImp(identity(), ext -> ext.publisherDomain(""))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -464,13 +476,15 @@ public void makeHttpRequestShouldAddEmptyDomainIfNoOtherSources() { .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) .extracting(Site::getDomain) - .containsExactly(""); + .containsExactly("domain"); } @Test - public void makeHttpRequestShouldCreateSiteIfNotPresent() { + public void makeHttpRequestShouldAddEmptyDomainIfNoOtherSources() { // given - final BidRequest bidRequest = givenBidRequest(givenBannerImp(identity(), identity())); + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().build()), + givenBannerImp(identity(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -480,13 +494,15 @@ public void makeHttpRequestShouldCreateSiteIfNotPresent() { assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) - .doesNotContainNull(); + .extracting(Site::getDomain) + .containsExactly(""); } @Test public void makeHttpRequestShouldUseDataFromLastImpExtForRequest() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().build()), givenBannerImp(identity(), ext -> ext.publisherId("1")), givenBannerImp(identity(), ext -> ext.publisherId("2"))); diff --git a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java new file mode 100644 index 00000000000..4fee1810ad3 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java @@ -0,0 +1,459 @@ +package org.prebid.server.bidder.thetradedesk; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class TheTradeDeskBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/{{SupplyId}}"; + + private final TheTradeDeskBidder target = new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supplyid"); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder("invalid_url", jacksonMapper, "supplyid")); + } + + @Test + public void creationShouldFailWhenSupplyIdHasNumbers() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supplyid123")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void creationShouldFailWhenSupplyIdHasSpecificCharacters() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supply_id")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void creationShouldFailWhenSupplyIdHasCapitalLetters() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supplyId")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void creationShouldFailWhenSupplyIdHasWhitespaces() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supply id")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get("x-integration-type")) + .isEqualTo("1")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/supplyid"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + imp -> imp.id("givenImp1"), + imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Set.of("givenImp1", "givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Cannot deserialize value"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnSiteWithExtImpPublisherWhenSiteAndAppArePresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(Publisher.builder().id("sitePublisher").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(Publisher.builder().id("newPublisher").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnSiteWithExtImpPublisherWhenSiteWithoutPublisherAndAppArePresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(null).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(Publisher.builder().id("newPublisher").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnAppWithExtImpPublisherWhenSiteIsAbsentAndAppIsPresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(null) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .app(App.builder().publisher(Publisher.builder().id("newPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnAppWithExtImpPublisherWhenAppWithoutPublisherArePresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(null) + .app(App.builder().publisher(null).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .app(App.builder().publisher(Publisher.builder().id("newPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnAppWithPublisherOfTheFirsrExtImp() { + final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().build()), + imp -> imp.ext(impExt("newPublisher")), + imp -> imp.ext(impExt("ignoredPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request.app(App.builder().publisher(Publisher.builder().id("newPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher")), + imp -> imp.ext(impExt("ignoredPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnBannerImpWithTheFirstFormat() { + final BidRequest bidRequest = givenBidRequest( + identity(), + imp -> imp.banner(Banner.builder() + .h(1) + .w(2) + .format(List.of( + Format.builder().h(11).w(22).build(), + Format.builder().h(111).w(222).build())) + .build()), + imp -> imp.banner(null).video(Video.builder().build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + identity(), + imp -> imp.banner(Banner.builder() + .h(11) + .w(22) + .format(List.of( + Format.builder().h(11).w(22).build(), + Format.builder().h(111).w(222).build())) + .build()), + imp -> imp.video(Video.builder().build())); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse("unsupported mtype: null")); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator... impCustomizers) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(TheTradeDeskBidderTest::givenImp).toList())) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("123").ext(impExt("publisherId"))).build(); + } + + private static ObjectNode impExt(String publisherId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId))); + } + +} diff --git a/src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java b/src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java new file mode 100644 index 00000000000..12384199d76 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java @@ -0,0 +1,330 @@ +package org.prebid.server.bidder.tradplus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.tradplus.ExtImpTradPlus; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.tradplus.TradPlusBidder.X_OPENRTB_VERSION; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER; + +public class TradPlusBidderTest extends VertxTest { + + private static final String ENDPOINT_TEMPLATE = "http://{{ZoneID}}/openrtb2?sid={{AccountID}}"; + + private final TradPlusBidder target = new TradPlusBidder(ENDPOINT_TEMPLATE, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new TradPlusBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldRemoveAllImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1"), + imp -> imp.id("impId2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsOnlyNulls(); + } + + @Test + public void makeHttpRequestsShouldMakeSingleRequestForAllImps() { + + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(2); + + assertThat(result.getValue()).hasSize(1) + .flatExtracting(HttpRequest::getImpIds) + .containsOnly("givenImp1", "givenImp2"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(X_OPENRTB_VERSION_HEADER)) + .isEqualTo(X_OPENRTB_VERSION)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTradPlus.of("testAccountId", "testZoneId")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getUri()).isEqualTo("http://testZoneId/openrtb2?sid=testAccountId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Cannot deserialize value"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenAccountIdIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTradPlus.of(null, "testZoneId")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Invalid/Missing AccountID")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenAccountIdIsBlank() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTradPlus.of(" ", "testZoneId")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Invalid/Missing AccountID")); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Failed to decode: Unrecognized token"); + assertThat(result.getErrors().getFirst().getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidImpIdIsNotPresent() throws JsonProcessingException { + // given + final BidderCall bidderCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + givenBidResponse(bidBuilder -> bidBuilder.impid("wrongBlock"))); + + // when + final Result> result = target.makeBids(bidderCall, null); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse( + "Invalid bid imp ID #wrongBlock does not match any imp IDs from the original bid request")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .video(Video.builder().build()) + .id("123") + .build())) + .build(), + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .xNative(Native.builder().build()) + .id("123") + .build())) + .build(), + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(TradPlusBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpTradPlus.of("accountId", "zoneId"))))) + .build(); + } + + private static String givenBidResponse(Function bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java b/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java index 00c1c100a9c..f5446b4c647 100644 --- a/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java @@ -101,7 +101,7 @@ public void makeHttpRequestsShouldReturnExpectedHeaders() { public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { // given final Imp givenInvalidImp = givenImp(imp -> imp - .id("impId") + .id("impIdCorrupted") .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); final Imp givenValidImp = givenImp(identity()); diff --git a/src/test/java/org/prebid/server/bidder/liftoff/LiftoffBidderTest.java b/src/test/java/org/prebid/server/bidder/vungle/VungleBidderTest.java similarity index 91% rename from src/test/java/org/prebid/server/bidder/liftoff/LiftoffBidderTest.java rename to src/test/java/org/prebid/server/bidder/vungle/VungleBidderTest.java index 84e06443cad..e609e2055f8 100644 --- a/src/test/java/org/prebid/server/bidder/liftoff/LiftoffBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/vungle/VungleBidderTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.liftoff; +package org.prebid.server.bidder.vungle; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -18,17 +18,17 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.liftoff.model.LiftoffImpressionExt; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.vungle.model.VungleImpressionExt; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; import java.math.BigDecimal; import java.util.Arrays; @@ -48,23 +48,23 @@ import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @ExtendWith(MockitoExtension.class) -public class LiftoffBidderTest extends VertxTest { +public class VungleBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; - private LiftoffBidder target; + private VungleBidder target; @BeforeEach public void setUp() { - target = new LiftoffBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + target = new VungleBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new LiftoffBidder( + assertThatIllegalArgumentException().isThrownBy(() -> new VungleBidder( "invalid_url", currencyConversionService, jacksonMapper)); @@ -176,7 +176,7 @@ public void makeHttpRequestsShouldThrowErrorWhenCurrencyConvertCannotConvertInAn } @Test - public void makeHttpRequestShouldUpdateExtImpLiftoffWhenUserBuyeruidPresent() { + public void makeHttpRequestShouldUpdateExtImpVungleWhenUserBuyeruidPresent() { // given final BidRequest bidRequest = givenBidRequest( bidRequestBuilder -> bidRequestBuilder.user(User.builder().buyeruid("123").build()), identity()); @@ -190,12 +190,12 @@ public void makeHttpRequestShouldUpdateExtImpLiftoffWhenUserBuyeruidPresent() { .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) - .containsExactly(mapper.convertValue(LiftoffImpressionExt.builder() - .bidder(ExtImpLiftoff.of( + .containsExactly(mapper.convertValue(VungleImpressionExt.builder() + .bidder(ExtImpVungle.of( "any-bid-token", "any-app-store-id", "any-placement-reference-id")) - .vungle(ExtImpLiftoff.of( + .vungle(ExtImpVungle.of( "123", "any-app-store-id", "any-placement-reference-id")) @@ -203,7 +203,7 @@ public void makeHttpRequestShouldUpdateExtImpLiftoffWhenUserBuyeruidPresent() { } @Test - public void makeHttpRequestShouldUpdateAppIdWhenExtImpLiftoffContainAppStoreId() { + public void makeHttpRequestShouldUpdateAppIdWhenExtImpVungleContainAppStoreId() { // given final App givenApp = App.builder().name("appName").build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.app(givenApp), identity()); @@ -220,7 +220,7 @@ public void makeHttpRequestShouldUpdateAppIdWhenExtImpLiftoffContainAppStoreId() } @Test - public void makeHttpRequestShouldCreateAppWithIdWhenExtImpLiftoffContainAppStoreIdAndAppIsAbsentAndSiteIsPresent() { + public void makeHttpRequestShouldCreateAppWithIdWhenExtImpVungleContainAppStoreIdAndAppIsAbsentAndSiteIsPresent() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder .site(Site.builder().build()) @@ -258,7 +258,7 @@ public void makeHttpRequestShouldReturnErrorWhenAppAndSiteAreAbsent() { } @Test - public void makeHttpRequestShouldUpdateImpTagidWhenExtImpLiftoffContainPlacementReferenceId() { + public void makeHttpRequestShouldUpdateImpTagidWhenExtImpVungleContainPlacementReferenceId() { // given final BidRequest bidRequest = givenBidRequest(identity(), identity()); @@ -275,7 +275,7 @@ public void makeHttpRequestShouldUpdateImpTagidWhenExtImpLiftoffContainPlacement } @Test - public void makeHttpRequestShouldUpdateExtImpLiftoffBidTokenWhenInRequestPresentUserBuyeruid() { + public void makeHttpRequestShouldUpdateExtImpVungleBidTokenWhenInRequestPresentUserBuyeruid() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.user(User.builder().buyeruid("Came-from-request").build()), identity()); @@ -289,11 +289,11 @@ public void makeHttpRequestShouldUpdateExtImpLiftoffBidTokenWhenInRequestPresent .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) - .containsExactly(mapper.convertValue(LiftoffImpressionExt.builder() - .bidder(ExtImpLiftoff.of("any-bid-token", + .containsExactly(mapper.convertValue(VungleImpressionExt.builder() + .bidder(ExtImpVungle.of("any-bid-token", "any-app-store-id", "any-placement-reference-id")) - .vungle(ExtImpLiftoff.of( + .vungle(ExtImpVungle.of( "Came-from-request", "any-app-store-id", "any-placement-reference-id")) @@ -386,7 +386,7 @@ private static BidRequest givenBidRequest( return bidRequestCustomizer.apply(BidRequest.builder() .app(App.builder().build()) - .imp(Arrays.stream(impCustomizer).map(LiftoffBidderTest::givenImp).toList())) + .imp(Arrays.stream(impCustomizer).map(VungleBidderTest::givenImp).toList())) .build(); } @@ -395,7 +395,7 @@ private static Imp givenImp(UnaryOperator impCustomizer) { .id("123") .banner(Banner.builder().w(23).h(25).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpLiftoff.of("any-bid-token", + ExtImpVungle.of("any-bid-token", "any-app-store-id", "any-placement-reference-id"))))) .build(); diff --git a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java index c4ba2bdd8bb..a445a285955 100644 --- a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java @@ -7,6 +7,7 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; @@ -25,9 +26,9 @@ import org.prebid.server.bidder.yieldlab.model.YieldlabDigitalServicesActResponse; import org.prebid.server.bidder.yieldlab.model.YieldlabResponse; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; -import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.yieldlab.ExtImpYieldlab; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -100,12 +101,15 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { targeting.put("key2", "value2"); final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() - .banner(Banner.builder().w(1).h(1).build()) + .banner(Banner.builder() + .format(List.of( + Format.builder().w(1).h(1).build(), + Format.builder().w(2).h(2).build())) + .build()) .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(targeting) .extId("extId") .build()))) @@ -127,8 +131,8 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { .extracting(HttpRequest::getUri) .allSatisfy(uri -> { assertThat(uri).startsWith("https://test.endpoint.com/1?content=json&pvid=true&ts="); - assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&ids=buyeruid&yl_rtb_ifa&" - + "yl_rtb_devicetype=1&gdpr=1&consent=consent"); + assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&sizes=1%3A1%7C1%2C1%3A2%7C2&" + + "ids=buyeruid&yl_rtb_ifa&yl_rtb_devicetype=1&gdpr=1&consent=consent"); final String ts = uri.substring(54, uri.indexOf("&t=")); assertThat(Long.parseLong(ts)).isEqualTo(expectedTime); }); @@ -157,7 +161,6 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(targeting) .extId("extId") .build()))) @@ -168,7 +171,6 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { ExtImpYieldlab.builder() .adslotId("2") .supplyId("2") - .adSize("adSize") .targeting(targeting) .extId("extId") .build()))) @@ -220,7 +222,6 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(singletonMap("key", "value")) .extId("extId") .build()))) @@ -272,7 +273,6 @@ public void makeBidsShouldReturnCorrectAdm() throws JsonProcessingException { .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("12345") .supplyId("123456789") - .adSize("728x90") .extId("abc") .build()))) .video(Video.builder().build()) @@ -452,7 +452,6 @@ public void makeBidsShouldAddDsaParamsWhenDsaIsPresentInResponse() throws JsonPr .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(singletonMap("key", "value")) .extId("extId") .build()))) diff --git a/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java b/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java index 3a146a26dc4..c7b09518ae7 100644 --- a/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java +++ b/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java @@ -225,10 +225,10 @@ public void storeEntryShouldCreateCallWithApiKeyInHeader() { } @Test - public void retrieveModuleEntryShouldReturnFailedFutureIfModuleKeyIsMissed() { + public void retrieveModuleEntryShouldReturnFailedFutureIfKeyIsMissed() { // when final Future result = - target.retrieveModuleEntry(null, "some-module-code", "some-app"); + target.retrieveEntry(null, "some-module-code", "some-app"); // then assertThat(result.failed()).isTrue(); @@ -237,10 +237,10 @@ public void retrieveModuleEntryShouldReturnFailedFutureIfModuleKeyIsMissed() { } @Test - public void retrieveModuleEntryShouldReturnFailedFutureIfModuleApplicationIsMissed() { + public void retrieveModuleEntryShouldReturnFailedFutureIfApplicationIsMissed() { // when final Future result = - target.retrieveModuleEntry("some-key", "some-module-code", null); + target.retrieveEntry("some-key", "some-module-code", null); // then assertThat(result.failed()).isTrue(); @@ -249,10 +249,10 @@ public void retrieveModuleEntryShouldReturnFailedFutureIfModuleApplicationIsMiss } @Test - public void retrieveModuleEntryShouldReturnFailedFutureIfModuleCodeIsMissed() { + public void retrieveModuleEntryShouldReturnFailedFutureIfCodeIsMissed() { // when final Future result = - target.retrieveModuleEntry("some-key", null, "some-app"); + target.retrieveEntry("some-key", null, "some-app"); // then assertThat(result.failed()).isTrue(); @@ -261,9 +261,9 @@ public void retrieveModuleEntryShouldReturnFailedFutureIfModuleCodeIsMissed() { } @Test - public void retrieveModuleEntryShouldCreateCallWithApiKeyInHeader() { + public void retrieveEntryShouldCreateCallWithApiKeyInHeader() { // when - target.retrieveModuleEntry("some-key", "some-module-code", "some-app"); + target.retrieveEntry("some-key", "some-module-code", "some-app"); // then final MultiMap result = captureRetrieveRequestHeaders(); @@ -271,9 +271,9 @@ public void retrieveModuleEntryShouldCreateCallWithApiKeyInHeader() { } @Test - public void retrieveModuleEntryShouldCreateCallWithKeyInParams() { + public void retrieveEntryShouldCreateCallWithKeyInParams() { // when - target.retrieveModuleEntry("some-key", "some-module-code", "some-app"); + target.retrieveEntry("some-key", "some-module-code", "some-app"); // then final String result = captureRetrieveUrl(); @@ -282,10 +282,10 @@ public void retrieveModuleEntryShouldCreateCallWithKeyInParams() { } @Test - public void retrieveModuleEntryShouldReturnExpectedResponse() { + public void retrieveEntryShouldReturnExpectedResponse() { // when final Future result = - target.retrieveModuleEntry("some-key", "some-module-code", "some-app"); + target.retrieveEntry("some-key", "some-module-code", "some-app"); // then assertThat(result.result()) diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 8c35236c361..058f958fa48 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -8,6 +8,7 @@ import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,8 +32,8 @@ import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.UUIDIdGenerator; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; @@ -40,6 +41,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.settings.model.Account; +import org.prebid.server.util.HttpUtil; import org.prebid.server.vast.VastModifier; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; @@ -107,6 +109,8 @@ public void setUp() throws MalformedURLException, JsonProcessingException { new URL("http://cache-service/cache"), "http://cache-service-host/cache?uuid=", 100L, + null, + false, vastModifier, eventsService, metrics, @@ -371,6 +375,40 @@ public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessin .build()); } + @Test + public void cacheBidsOpenrtbShouldUseApiKeyWhenProvided() throws MalformedURLException { + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + "ApiKey", + true, + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); + + // when + final Future future = target.cacheBidsOpenrtb( + singletonList(bidinfo), + givenAuctionContext(), + CacheContext.builder() + .shouldCacheBids(true) + .build(), + eventsContext); + + // then + assertThat(future.result().getHttpCall().getRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString())) + .containsExactly("ApiKey"); + assertThat(captureBidCacheRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString())) + .isEqualTo("ApiKey"); + } + @Test public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() { // given @@ -646,7 +684,6 @@ public void cacheBidsOpenrtbShouldNotUpdateVastXmlPutObjectWithKeyWhenDoesNotHav @Test public void cacheBidsOpenrtbShouldRemoveCatDurPrefixFromVideoUuidFromResponse() throws IOException { // given - givenHttpClientReturnsResponse(200, mapper.writeValueAsString( BidCacheResponse.of(asList(CacheObject.of("uuid"), CacheObject.of("catDir_randomId"))))); final BidInfo bidInfo1 = givenBidInfo(builder -> builder.id("bid1").impid("impId1").adm("adm"), @@ -695,7 +732,7 @@ public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty( } @Test - public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOException { + public void cachePutObjectsShould() throws IOException { // given final BidPutObject firstBidPutObject = BidPutObject.builder() .type("json") @@ -763,6 +800,45 @@ public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOExcepti .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); } + @Test + public void cachePutObjectsShouldUseApiKeyWhenProvided() throws MalformedURLException { + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + "ApiKey", + true, + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + + final BidPutObject firstBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .build(); + + // when + target.cachePutObjects( + asList(firstBidPutObject), + true, + singleton("bidder1"), + "account", + "pbjs", + timeout); + + // then + assertThat(captureBidCacheRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString())) + .isEqualTo("ApiKey"); + } + private AuctionContext givenAuctionContext(UnaryOperator accountCustomizer, UnaryOperator bidRequestCustomizer) { @@ -851,6 +927,12 @@ private BidCacheRequest captureBidCacheRequest() throws IOException { return mapper.readValue(captor.getValue(), BidCacheRequest.class); } + private MultiMap captureBidCacheRequestHeaders() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient).post(anyString(), captor.capture(), anyString(), anyLong()); + return captor.getValue(); + } + private Map> givenDebugHeaders() { final Map> headers = new HashMap<>(); headers.put("Accept", singletonList("application/json")); diff --git a/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java b/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java new file mode 100644 index 00000000000..407e01c6bf5 --- /dev/null +++ b/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java @@ -0,0 +1,68 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.assertion.FutureAssertion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class LocalFileSupplierTest { + + @Mock + private FileSystem fileSystem; + + private LocalFileSupplier target; + + @BeforeEach + public void setUp() { + given(fileSystem.exists(anyString())).willReturn(Future.succeededFuture(true)); + + target = new LocalFileSupplier("/path/to/file", fileSystem); + } + + @Test + public void getShouldReturnFailedFutureIfFileNotFound() { + // given + given(fileSystem.exists(anyString())).willReturn(Future.succeededFuture(false)); + + // when and then + FutureAssertion.assertThat(target.get()).isFailed().hasMessage("File /path/to/file not found."); + } + + @Test + public void getShouldReturnFilePath() { + // given + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.props(anyString())).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.creationTime()).willReturn(1000L); + + // when and then + assertThat(target.get().result()).isEqualTo("/path/to/file"); + } + + @Test + public void getShouldReturnNullIfFileNotModifiedSinceLastTry() { + // given + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.props(anyString())).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.creationTime()).willReturn(1000L); + + // when + target.get(); + final Future result = target.get(); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isNull(); + } +} diff --git a/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java b/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java new file mode 100644 index 00000000000..d60ff696ba7 --- /dev/null +++ b/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java @@ -0,0 +1,244 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.file.AsyncFile; +import io.vertx.core.file.CopyOptions; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.assertion.FutureAssertion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class RemoteFileSupplierTest { + + private static final String SAVE_PATH = "/path/to/file"; + private static final String BACKUP_PATH = SAVE_PATH + ".old"; + private static final String TMP_PATH = "/path/to/tmp"; + + @Mock + private HttpClient httpClient; + + @Mock + private FileSystem fileSystem; + + private RemoteFileSupplier target; + + @Mock + private HttpClientResponse getResponse; + + @Mock + private HttpClientResponse headResponse; + + @BeforeEach + public void setUp() { + final HttpClientRequest getRequest = mock(HttpClientRequest.class); + given(httpClient.request(argThat(requestOptions -> + requestOptions != null && requestOptions.getMethod().equals(HttpMethod.GET)))) + .willReturn(Future.succeededFuture(getRequest)); + given(getRequest.send()).willReturn(Future.succeededFuture(getResponse)); + + final HttpClientRequest headRequest = mock(HttpClientRequest.class); + given(httpClient.request(argThat(requestOptions -> + requestOptions != null && requestOptions.getMethod().equals(HttpMethod.HEAD)))) + .willReturn(Future.succeededFuture(headRequest)); + given(headRequest.send()).willReturn(Future.succeededFuture(headResponse)); + given(headResponse.statusCode()).willReturn(200); + + target = target(false); + } + + private RemoteFileSupplier target(boolean checkRemoteFileSize) { + return new RemoteFileSupplier( + "https://download.url/", + SAVE_PATH, + TMP_PATH, + httpClient, + 1000L, + checkRemoteFileSize, + fileSystem); + } + + @Test + public void shouldCheckWritePermissionsForFiles() { + // given + reset(fileSystem); + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.existsBlocking(anyString())).willReturn(true); + given(fileSystem.propsBlocking(anyString())).willReturn(fileProps); + given(fileProps.isDirectory()).willReturn(false); + + // when + target(false); + + // then + verify(fileSystem, times(3)).mkdirsBlocking(anyString()); + } + + @Test + public void getShouldReturnFailureWhenCanNotOpenTmpFile() { + // given + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.failedFuture("Failure.")); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Promise.promise().future()); + + // when + final Future result = target.get(); + + // then + FutureAssertion.assertThat(result).isFailed().hasMessage("Failure."); + } + + @Test + public void getShouldReturnFailureOnNotOkStatusCode() { + // given + final AsyncFile tmpFile = mock(AsyncFile.class); + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile)); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Promise.promise().future()); + + given(getResponse.statusCode()).willReturn(204); + + // when + final Future result = target.get(); + + // then + FutureAssertion.assertThat(result).isFailed() + .hasMessage("Got unexpected response from server with status code 204 and message null"); + } + + @Test + public void getShouldReturnExpectedResult() { + // given + final AsyncFile tmpFile = mock(AsyncFile.class); + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile)); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.move(eq(SAVE_PATH), eq(BACKUP_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + given(fileSystem.move(eq(TMP_PATH), eq(SAVE_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + + given(getResponse.statusCode()).willReturn(200); + given(getResponse.pipeTo(any())).willReturn(Future.succeededFuture()); + + // when + final Future result = target.get(); + + // then + verify(tmpFile).close(); + assertThat(result.result()).isEqualTo(SAVE_PATH); + } + + @Test + public void getShouldReturnExpectedResultWhenCheckRemoteFileSizeIsTrue() { + // given + target = target(true); + + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.props(eq(SAVE_PATH))).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.size()).willReturn(1000L); + + given(headResponse.statusCode()).willReturn(200); + given(headResponse.getHeader(eq(HttpHeaders.CONTENT_LENGTH))).willReturn("1001"); + + final AsyncFile tmpFile = mock(AsyncFile.class); + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile)); + given(fileSystem.move(eq(SAVE_PATH), eq(BACKUP_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + given(fileSystem.move(eq(TMP_PATH), eq(SAVE_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + + given(getResponse.statusCode()).willReturn(200); + given(getResponse.pipeTo(any())).willReturn(Future.succeededFuture()); + + // when + final Future result = target.get(); + + // then + verify(tmpFile).close(); + assertThat(result.result()).isEqualTo(SAVE_PATH); + } + + @Test + public void getShouldReturnNullWhenCheckRemoteFileSizeIsTrueAndSizeNotChanged() { + // given + target = target(true); + + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.props(eq(SAVE_PATH))).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.size()).willReturn(1000L); + + given(headResponse.statusCode()).willReturn(200); + given(headResponse.getHeader(eq(HttpHeaders.CONTENT_LENGTH))).willReturn("1000"); + + // when + final Future result = target.get(); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void clearTmpShouldCallExpectedMethods() { + // given + given(fileSystem.exists(eq(TMP_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.delete(eq(TMP_PATH))).willReturn(Future.succeededFuture()); + + // when + target.clearTmp(); + + // then + verify(fileSystem).delete(TMP_PATH); + } + + @Test + public void deleteBackupShouldCallExpectedMethods() { + // given + given(fileSystem.exists(eq(BACKUP_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.delete(eq(BACKUP_PATH))).willReturn(Future.succeededFuture()); + + // when + target.deleteBackup(); + + // then + verify(fileSystem).delete(BACKUP_PATH); + } + + @Test + public void restoreFromBackupShouldCallExpectedMethods() { + // given + given(fileSystem.exists(eq(BACKUP_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.move(eq(BACKUP_PATH), eq(SAVE_PATH))).willReturn(Future.succeededFuture()); + given(fileSystem.delete(eq(BACKUP_PATH))).willReturn(Future.succeededFuture()); + + // when + target.deleteBackup(); + + // then + verify(fileSystem).delete(BACKUP_PATH); + } +} diff --git a/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java b/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java new file mode 100644 index 00000000000..39a8e8ae966 --- /dev/null +++ b/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java @@ -0,0 +1,160 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; +import org.prebid.server.execution.retry.NonRetryable; +import org.prebid.server.execution.retry.RetryPolicy; +import org.testcontainers.shaded.org.apache.commons.lang3.NotImplementedException; + +import java.util.concurrent.Callable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class FileSyncerTest { + + private static final String SAVE_PATH = "/path/to/file"; + + @Mock + private FileProcessor fileProcessor; + + @Mock + private Vertx vertx; + + @BeforeEach + public void setUp() { + given(vertx.executeBlocking(Mockito.>any())).willAnswer(invocation -> { + try { + return Future.succeededFuture(((Callable) invocation.getArgument(0)).call()); + } catch (Throwable e) { + return Future.failedFuture(e); + } + }); + } + + @Test + public void syncShouldCallExpectedMethodsOnSuccessWhenNoReturnedFile() { + // given + final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance()); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture()); + + // when + fileSyncer.sync(); + + // then + verifyNoInteractions(fileProcessor); + verify(fileSyncer).doOnSuccess(); + verify(vertx).setTimer(eq(1000L), any()); + } + + @Test + public void syncShouldCallExpectedMethodsOnSuccess() { + // given + final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance()); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH)); + given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.succeededFuture()); + + // when + fileSyncer.sync(); + + // then + verify(fileProcessor).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer).doOnSuccess(); + verify(vertx).setTimer(eq(1000L), any()); + } + + @Test + public void syncShouldCallExpectedMethodsOnFailure() { + // given + final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance()); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH)); + given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.failedFuture("Failure")); + + // when + fileSyncer.sync(); + + // then + verify(fileProcessor).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer).doOnFailure(any()); + verify(vertx).setTimer(eq(1000L), any()); + } + + @Test + public void syncShouldCallExpectedMethodsOnFailureWithRetryable() { + // given + final FileSyncer fileSyncer = fileSyncer(FixedIntervalRetryPolicy.limited(10L, 1)); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH)); + given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.failedFuture("Failure")); + + final Promise promise = Promise.promise(); + given(vertx.setTimer(eq(10L), any())).willAnswer(invocation -> { + promise.future().onComplete(ignore -> ((Handler) invocation.getArgument(1)).handle(1L)); + return 1L; + }); + + // when + fileSyncer.sync(); + + // then + verify(fileProcessor).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer).doOnFailure(any()); + verify(vertx).setTimer(eq(10L), any()); + + // when + promise.complete(); + + // then + verify(fileProcessor, times(2)).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer, times(2)).doOnFailure(any()); + verify(vertx).setTimer(eq(1000L), any()); + } + + private FileSyncer fileSyncer(RetryPolicy retryPolicy) { + return spy(new TestFileSyncer(fileProcessor, 1000L, retryPolicy, vertx)); + } + + private static class TestFileSyncer extends FileSyncer { + + protected TestFileSyncer(FileProcessor fileProcessor, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + } + + @Override + public Future getFile() { + return Future.failedFuture(new NotImplementedException()); + } + + @Override + protected Future doOnSuccess() { + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + return Future.succeededFuture(); + } + } +} diff --git a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java b/src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java similarity index 70% rename from src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java rename to src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java index 341c7764cb3..2da614c4e50 100644 --- a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java +++ b/src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java @@ -1,5 +1,6 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.file.syncer; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -16,14 +17,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileProcessor; import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; import org.prebid.server.execution.retry.RetryPolicy; import java.io.File; +import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -56,7 +60,8 @@ public class RemoteFileSyncerTest extends VertxTest { private static final String TMP_FILE_PATH = String.join(File.separator, "tmp", "fake", "path", "to", "file.pdf"); private static final String DIR_PATH = String.join(File.separator, "fake", "path", "to"); private static final Long FILE_SIZE = 2131242L; - @Mock + + @Mock(strictness = LENIENT) private Vertx vertx; @Mock(strictness = LENIENT) @@ -66,7 +71,7 @@ public class RemoteFileSyncerTest extends VertxTest { private HttpClient httpClient; @Mock(strictness = LENIENT) - private RemoteFileProcessor remoteFileProcessor; + private FileProcessor fileProcessor; @Mock private AsyncFile asyncFile; @@ -84,30 +89,38 @@ public class RemoteFileSyncerTest extends VertxTest { @BeforeEach public void setUp() { when(vertx.fileSystem()).thenReturn(fileSystem); - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + given(vertx.executeBlocking(Mockito.>any())).willAnswer(invocation -> { + try { + return Future.succeededFuture(((Callable) invocation.getArgument(0)).call()); + } catch (Throwable e) { + return Future.failedFuture(e); + } + }); + + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, 0, httpClient, vertx); } @Test public void shouldThrowNullPointerExceptionWhenIllegalArgumentsWhenNullArguments() { assertThatNullPointerException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, null, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, + () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, null, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)); assertThatNullPointerException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, null, vertx)); assertThatNullPointerException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, null)); } @Test public void shouldThrowIllegalArgumentExceptionWhenIllegalArguments() { assertThatIllegalArgumentException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, null, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, null, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)); assertThatIllegalArgumentException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, "bad url", FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, "bad url", FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)); } @@ -118,7 +131,7 @@ public void creteShouldCreateDirWithWritePermissionIfDirNotExist() { when(fileSystem.existsBlocking(eq(DIR_PATH))).thenReturn(false); // when - new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, + new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); // then @@ -135,7 +148,7 @@ public void createShouldCreateDirWithWritePermissionIfItsNotDir() { when(fileProps.isDirectory()).thenReturn(false); // when - new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, + new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); // then @@ -150,7 +163,7 @@ public void createShouldThrowPreBidExceptionWhenPropsThrowException() { when(fileSystem.propsBlocking(eq(DIR_PATH))).thenThrow(FileSystemException.class); // when and then - assertThatThrownBy(() -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, + assertThatThrownBy(() -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)) .isInstanceOf(PreBidException.class); } @@ -166,14 +179,14 @@ public void syncForFilepathShouldNotTriggerServiceWhenCantCheckIfUsableFileExist // then verify(fileSystem).exists(eq(FILE_PATH)); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); verifyNoInteractions(httpClient); } @Test public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { // given - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -182,6 +195,8 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); given(httpClientResponse.getHeader(HttpHeaders.CONTENT_LENGTH)) .willReturn("notnumber"); @@ -191,7 +206,7 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { // then verify(fileSystem, times(2)).exists(eq(FILE_PATH)); verify(httpClient).request(any()); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(), any()); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -200,7 +215,7 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { @Test public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { // given - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -209,7 +224,10 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.failedFuture(new IllegalArgumentException("ERROR"))); @@ -222,7 +240,7 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { verify(httpClient).request(any()); verify(httpClientResponse).getHeader(eq(HttpHeaders.CONTENT_LENGTH)); verify(fileSystem).props(eq(FILE_PATH)); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -231,7 +249,7 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { @Test public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { // given - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -240,7 +258,10 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.succeededFuture(fileProps)); @@ -255,7 +276,7 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { verify(httpClient).request(any()); verify(httpClientResponse).getHeader(eq(HttpHeaders.CONTENT_LENGTH)); verify(fileSystem).props(eq(FILE_PATH)); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -265,7 +286,7 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { // given remoteFileSyncer = new RemoteFileSyncer( - remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -274,8 +295,12 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.pipeTo(any())).willReturn(Future.succeededFuture()); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.pipeTo(any())) + .willReturn(Future.succeededFuture()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.succeededFuture(fileProps)); @@ -291,7 +316,8 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { given(fileSystem.move(anyString(), any(), any(CopyOptions.class))) .willReturn(Future.succeededFuture()); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(fileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -305,7 +331,7 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { verify(fileSystem).open(eq(TMP_FILE_PATH), any()); verify(asyncFile).close(); - verify(remoteFileProcessor, times(2)).setDataPath(any()); + verify(fileProcessor, times(2)).setDataPath(any()); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verifyNoMoreInteractions(httpClient); @@ -333,7 +359,7 @@ public void syncForFilepathShouldRetryAfterFailedDownload() { verify(fileSystem, times(RETRY_COUNT + 1)).open(eq(TMP_FILE_PATH), any()); verifyNoInteractions(httpClient); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); } @Test @@ -354,7 +380,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())) .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture(new RuntimeException()))); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(fileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -364,13 +391,14 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { verify(fileSystem, times(RETRY_COUNT + 1)).delete(eq(TMP_FILE_PATH)); verifyNoInteractions(httpClient); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); } @Test public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotSet() { // given - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(fileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(false)); @@ -382,6 +410,8 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); given(httpClientResponse.pipeTo(asyncFile)) .willReturn(Future.succeededFuture()); @@ -395,7 +425,8 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS verify(fileSystem).open(eq(TMP_FILE_PATH), any()); verify(httpClient).request(any()); verify(asyncFile).close(); - verify(remoteFileProcessor).setDataPath(any()); + verify(httpClientResponse).statusCode(); + verify(fileProcessor).setDataPath(any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -419,8 +450,6 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { given(httpClient.request(any())) .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) - .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.pipeTo(asyncFile)) .willReturn(Future.failedFuture("Timeout")); // when @@ -429,19 +458,90 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { // then verify(vertx, times(RETRY_COUNT)).setTimer(eq(RETRY_INTERVAL), any()); verify(fileSystem, times(RETRY_COUNT + 1)).open(eq(TMP_FILE_PATH), any()); + verify(httpClientResponse, never()).pipeTo(any()); // Response handled verify(httpClient, times(RETRY_COUNT + 1)).request(any()); verify(asyncFile, times(RETRY_COUNT + 1)).close(); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); + } + + @Test + public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() { + // given + given(fileSystem.exists(anyString())) + .willReturn(Future.succeededFuture(false)); + given(fileSystem.open(any(), any())) + .willReturn(Future.succeededFuture(asyncFile)); + given(fileSystem.move(anyString(), anyString(), any(CopyOptions.class))) + .willReturn(Future.succeededFuture()); + + given(httpClient.request(any())) + .willReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()) + .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(0); + + // when + remoteFileSyncer.sync(); + + // then + verify(fileSystem, times(1)).exists(eq(FILE_PATH)); + verify(fileSystem).open(eq(TMP_FILE_PATH), any()); + verify(fileSystem).delete(eq(TMP_FILE_PATH)); + verify(asyncFile).close(); + verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); + verify(httpClient).request(any()); + verify(httpClientResponse).statusCode(); + verify(httpClientResponse, never()).pipeTo(any()); + verify(fileProcessor, never()).setDataPath(any()); + verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); + } + + @Test + public void syncShouldNotUpdateFileWhenServerRespondsWithNonOkStatusCode() { + // given + remoteFileSyncer = new RemoteFileSyncer( + fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); + + givenTriggerUpdate(); + + given(fileSystem.open(any(), any())) + .willReturn(Future.succeededFuture(asyncFile)); + given(fileSystem.move(anyString(), anyString(), any(CopyOptions.class))) + .willReturn(Future.succeededFuture()); + + given(httpClient.request(any())) + .willReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()) + .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(0); + + // when + remoteFileSyncer.sync(); + + // then + verify(fileSystem, times(1)).exists(eq(FILE_PATH)); + verify(fileSystem, never()).open(any(), any()); + verify(fileSystem, never()).delete(any()); + verify(fileSystem, never()).move(any(), any(), any(), any()); + verify(asyncFile, never()).close(); + verify(httpClient, times(1)).request(any()); + verify(httpClientResponse).statusCode(); + verify(httpClientResponse, never()).pipeTo(any()); + verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); } private void givenTriggerUpdate() { given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(true)); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(fileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(vertx.setPeriodic(eq(UPDATE_INTERVAL), any())) .willAnswer(withReturnObjectAndPassObjectToHandler(123L, 123L, 1)) diff --git a/src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java b/src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java similarity index 97% rename from src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java rename to src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java index e38e4c7304d..9e4a140cb7b 100644 --- a/src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java +++ b/src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/prebid/server/execution/TimeoutTest.java b/src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java similarity index 95% rename from src/test/java/org/prebid/server/execution/TimeoutTest.java rename to src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java index c45589bc86a..c1c7dfd7ca3 100644 --- a/src/test/java/org/prebid/server/execution/TimeoutTest.java +++ b/src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java index 38746f2b799..724aef1f391 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java @@ -347,7 +347,9 @@ public void shouldRejectBidsHavingPriceBelowFloor() { // then verify(priceFloorAdjuster, times(2)).revertAdjustmentForImp(any(), any(), any(), any()); - verify(rejectionTracker).reject("impId1", BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); + final BidderBid rejectedBid = BidderBid.of( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), null, null); + verify(rejectionTracker).rejectBid(rejectedBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); assertThat(singleton(result)) .extracting(AuctionParticipation::getBidderResponse) .extracting(BidderResponse::getSeatBid) diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 70835b4286a..8cc02e64a4a 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -118,9 +118,12 @@ public void shouldUseFloorsDataFromProviderIfPresent() { } @Test - public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { + public void shouldUseFloorsFromProviderIfUseDynamicDataAndUseFetchDataRateAreAbsent() { // given - final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com")); + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(null)); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when @@ -142,9 +145,65 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { } @Test - public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() { + public void shouldUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs100() { // given - final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com")); + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(100)); + + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), null), + givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)), + "bidder", + new ArrayList<>(), + new ArrayList<>()); + + // then + assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors + .enabled(true) + .skipped(false) + .floorProvider("provider.com") + .data(providerFloorsData) + .fetchStatus(FetchStatus.success) + .location(PriceFloorLocation.fetch))); + } + + @Test + public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs0() { + // given + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(0)); + + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), null), + givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)), + "bidder", + new ArrayList<>(), + new ArrayList<>()); + + // then + final PriceFloorRules actualRules = extractFloors(result); + assertThat(actualRules) + .extracting(PriceFloorRules::getFetchStatus) + .isEqualTo(FetchStatus.success); + assertThat(actualRules) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); + } + + @Test + public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrueAndUseFetchDataRateIsAbsent() { + // given + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(null)); given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when @@ -167,9 +226,37 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() { } @Test - public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() { + public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIsAbsent() { // given - final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com")); + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(null)); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), null), + givenAccount(floorsConfig -> floorsConfig.useDynamicData(false)), + "bidder", + new ArrayList<>(), + new ArrayList<>()); + + // then + final PriceFloorRules actualRules = extractFloors(result); + assertThat(actualRules) + .extracting(PriceFloorRules::getFetchStatus) + .isEqualTo(FetchStatus.success); + assertThat(actualRules) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); + } + + @Test + public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIs100() { + // given + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(100)); given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when @@ -493,7 +580,6 @@ public void shouldNotUpdateImpsIfBidFloorNotResolved() { @Test public void shouldUpdateImpsIfBidFloorResolved() { // given - final PriceFloorRules requestFloors = givenFloors(floors -> floors .data(givenFloorData(floorData -> floorData .modelGroups(singletonList(givenModelGroup(identity())))))); @@ -511,7 +597,6 @@ public void shouldUpdateImpsIfBidFloorResolved() { .willReturn(PriceFloorResult.of("rule", BigDecimal.ONE, BigDecimal.TEN, "USD")); // when - final BidRequest result = target.enrichWithPriceFloors( givenBidRequest(request -> request.imp(imps), requestFloors), givenAccount(identity()), diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java index 59253e200a7..64a66c507c0 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java @@ -12,7 +12,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.model.PriceFloorField; diff --git a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java index 77c4f56123c..5516bb5c9e1 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java @@ -62,6 +62,18 @@ public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() { .withMessage("Price floor data skipRate must be in range(0-100), but was -1"); } + @Test + public void validateShouldThrowExceptionOnInvalidUseFetchDataRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData( + dataBuilder -> dataBuilder.useFetchDataRate(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor data useFetchDataRate must be in range(0-100), but was -1"); + } + @Test public void validateShouldThrowExceptionOnAbsentDataModelGroups() { // given diff --git a/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java b/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java index 945d292970f..05643ce0e57 100644 --- a/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java +++ b/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.geolocation.model.GeoInfoConfiguration; diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index e5c71464d05..0b6027fcc92 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -32,7 +32,7 @@ import org.prebid.server.cookie.model.PartitionedCookie; import org.prebid.server.cookie.proto.Uids; import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.ccpa.Ccpa; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java index 9f27a7dde1f..c386ca753ef 100644 --- a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java @@ -19,7 +19,7 @@ import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.model.Tuple2; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.settings.ApplicationSettings; diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 5df909ebd78..95e5ad24ced 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -33,7 +33,7 @@ import org.prebid.server.cookie.proto.Uids; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.HostVendorTcfDefinerService; import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; diff --git a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java index e0f2e5a966c..0674ab1d1ce 100644 --- a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java @@ -21,7 +21,7 @@ import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.cache.proto.response.bid.CacheObject; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; diff --git a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java index e78e65fa082..d8589113f31 100644 --- a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java @@ -195,6 +195,7 @@ private static BidderInfo givenBidderInfo(boolean enabled, String endpoint, Stri singletonList(MediaType.NATIVE), null, 0, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index a5af550a562..aee1397e4c9 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -27,6 +27,7 @@ import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; @@ -39,36 +40,69 @@ import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtModules; import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; import java.time.Clock; import java.time.Instant; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.function.UnaryOperator; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static java.util.function.Function.identity; +import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; @@ -104,8 +138,15 @@ public class AmpHandlerTest extends VertxTest { private Clock clock; @Mock private HttpInteractionLogger httpInteractionLogger; + @Mock + private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; + + private AmpHandler target; - private AmpHandler ampHandler; @Mock private RoutingContext routingContext; @Mock(strictness = LENIENT) @@ -114,8 +155,6 @@ public class AmpHandlerTest extends VertxTest { private HttpServerResponse httpResponse; @Mock(strictness = LENIENT) private UidsCookie uidsCookie; - @Mock - private PrebidVersionProvider prebidVersionProvider; private Timeout timeout; @@ -139,19 +178,28 @@ public void setUp() { given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + timeout = new TimeoutFactory(clock).create(2000L); - ampHandler = new AmpHandler( + target = new AmpHandler( ampRequestFactory, exchangeService, analyticsReporterDelegator, metrics, + hooksMetricsService, clock, bidderCatalog, singleton("bidder1"), new AmpResponsePostProcessor.NoOpAmpResponsePostProcessor(), httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, jacksonMapper, 0); } @@ -165,7 +213,7 @@ public void shouldSetRequestTypeMetricToAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionContext auctionContext = captureAuctionContext(); @@ -181,7 +229,7 @@ public void shouldUseTimeoutFromAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -203,7 +251,7 @@ public void shouldAddPrebidVersionResponseHeader() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -225,7 +273,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -245,7 +293,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -263,7 +311,7 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -276,6 +324,7 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Invalid request format: Request is invalid")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -285,7 +334,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedAccount() { .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -297,6 +346,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedAccount() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Blocklisted: Blocklisted account")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -306,7 +356,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedApp() { .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -318,6 +368,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedApp() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Blocklisted: Blocklisted app")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -327,7 +378,7 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -339,6 +390,7 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Account id is not provided")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -348,7 +400,7 @@ public void shouldRespondWithBadRequestOnInvalidAccountConfigException() { .willReturn(Future.failedFuture(new InvalidAccountConfigException("Account is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -361,6 +413,7 @@ public void shouldRespondWithBadRequestOnInvalidAccountConfigException() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Invalid account configuration: Account is invalid")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -373,7 +426,7 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { .willThrow(new RuntimeException("Unexpected exception")); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); @@ -384,6 +437,7 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -398,7 +452,7 @@ public void shouldRespondWithInternalServerErrorIfCannotExtractBidTargeting() { givenHoldAuction(givenBidResponse(ext)); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); @@ -410,6 +464,7 @@ public void shouldRespondWithInternalServerErrorIfCannotExtractBidTargeting() { tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end( startsWith("Critical error while running the auction: Critical error while unpacking AMP targets:")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -421,10 +476,11 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -442,7 +498,7 @@ public void shouldRespondWithExpectedResponse() { givenHoldAuction(givenBidResponse(mapper.valueToTree(extPrebid))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()).hasSize(4) @@ -453,6 +509,68 @@ public void shouldRespondWithExpectedResponse() { tuple("Content-Type", "application/json"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithExpectedResponseWhenExitpointHookChangesResponseAndHeaders() { + // given + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + + final Map targeting = new HashMap<>(); + targeting.put("key1", "value1"); + targeting.put("hb_cache_id_bidder1", "value2"); + final ExtPrebid extPrebid = ExtPrebid.of( + ExtBidPrebid.builder().targeting(targeting).build(), + null); + givenHoldAuction(givenBidResponse(mapper.valueToTree(extPrebid))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"targeting\":{\"new-key\":\"new-value\"}}")))); + + // when + target.handle(routingContext); + + // then + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly(tuple("New-Header", "New-Header-Value")); + verify(httpResponse).end(eq("{\"targeting\":{\"new-key\":\"new-value\"}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -485,11 +603,18 @@ public void shouldRespondWithCustomTargetingIncluded() { willReturn(bidder).given(bidderCatalog).bidderByName(anyString()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq("{\"targeting\":{\"key1\":\"value1\",\"rpfl_11078\":\"15_tier0030\"," + "\"hb_cache_id_bidder1\":\"value2\"}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{\"key1\":\"value1\",\"rpfl_11078\":\"15_tier0030\"," + + "\"hb_cache_id_bidder1\":\"value2\"}}"), + any()); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -524,10 +649,15 @@ public void shouldRespondWithAdditionalTargetingIncludedWhenSeatBidExists() { .build()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -547,10 +677,15 @@ public void shouldRespondWithAdditionalTargetingIncludedWhenNoSeatBidExists() { givenHoldAuction(givenBidResponseWithExt(ExtBidResponse.builder().prebid(extBidResponsePrebid).build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -569,12 +704,19 @@ public void shouldRespondWithDebugInfoIncludedIfTestFlagIsTrue() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq( "{\"targeting\":{}," + "\"ext\":{\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{}," + + "\"ext\":{\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); + } @Test @@ -597,7 +739,7 @@ public void shouldRespondWithHooksDebugAndTraceOutput() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq( @@ -606,6 +748,15 @@ public void shouldRespondWithHooksDebugAndTraceOutput() { + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}}," + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}}," + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{}," + + "\"ext\":{\"prebid\":{\"modules\":{" + + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}}," + + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}}," + + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -618,7 +769,7 @@ public void shouldIncrementOkAmpRequestMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.ok)); @@ -634,7 +785,7 @@ public void shouldIncrementAppRequestMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); @@ -655,7 +806,7 @@ public void shouldIncrementNoCookieMetrics() { + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7"); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); @@ -672,7 +823,7 @@ public void shouldIncrementImpsRequestedMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); @@ -690,7 +841,7 @@ public void shouldIncrementImpsTypesMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateImpTypesMetrics(same(imps)); @@ -703,7 +854,7 @@ public void shouldIncrementBadinputAmpRequestMetrics() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.badinput)); @@ -716,7 +867,7 @@ public void shouldIncrementErrAmpRequestMetrics() { .willReturn(Future.failedFuture(new RuntimeException())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.err)); @@ -726,7 +877,6 @@ public void shouldIncrementErrAmpRequestMetrics() { @Test public void shouldUpdateRequestTimeMetric() { // given - // set up clock mock to check that request_time metric has been updated with expected value given(clock.millis()).willReturn(5000L).willReturn(5500L); @@ -742,7 +892,7 @@ public void shouldUpdateRequestTimeMetric() { }); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); @@ -755,7 +905,7 @@ public void shouldNotUpdateRequestTimeMetricIfRequestFails() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).endHandler(any()); @@ -778,7 +928,7 @@ public void shouldUpdateNetworkErrorMetric() { }); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr)); @@ -794,7 +944,7 @@ public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr)); @@ -812,7 +962,7 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr)); @@ -825,7 +975,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AmpEvent ampEvent = captureAmpEvent(); @@ -835,6 +985,8 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .status(400) .errors(singletonList("Invalid request format: Request is invalid")) .build()); + + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -848,7 +1000,7 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .willThrow(new RuntimeException("Unexpected exception")); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AmpEvent ampEvent = captureAmpEvent(); @@ -863,6 +1015,8 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .status(500) .errors(singletonList("Unexpected exception")) .build()); + + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -877,7 +1031,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AmpEvent ampEvent = captureAmpEvent(); @@ -890,33 +1044,317 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { .build())) .build())) .build(); - final AuctionContext expectedAuctionContext = auctionContext.toBuilder() - .requestTypeMetric(MetricName.amp) - .bidResponse(expectedBidResponse) + + assertThat(ampEvent.getHttpContext()).isEqualTo(givenHttpContext(singletonMap("Origin", "http://example.com"))); + assertThat(ampEvent.getBidResponse()).isEqualTo(expectedBidResponse); + assertThat(ampEvent.getTargeting()) + .isEqualTo(singletonMap("hb_cache_id_bidder1", TextNode.valueOf("value1"))); + assertThat(ampEvent.getOrigin()).isEqualTo("http://example.com"); + assertThat(ampEvent.getStatus()).isEqualTo(200); + assertThat(ampEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.amp); + assertThat(ampEvent.getAuctionContext().getBidResponse()).isEqualTo(expectedBidResponse); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"hb_cache_id_bidder1\":\"value1\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"targeting\":{\"new-key\":\"new-value\"}}")))); + + givenHoldAuction(givenBidResponse(mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(), + null)))); + + // when + target.handle(routingContext); + + // then + final AmpEvent ampEvent = captureAmpEvent(); + final BidResponse expectedBidResponse = BidResponse.builder().seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder() + .ext(mapper.valueToTree(ExtPrebid.of( + ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")) + .build(), + null))) + .build())) + .build())) .build(); - assertThat(ampEvent).isEqualTo(AmpEvent.builder() - .httpContext(givenHttpContext(singletonMap("Origin", "http://example.com"))) - .auctionContext(expectedAuctionContext) - .bidResponse(expectedBidResponse) - .targeting(singletonMap("hb_cache_id_bidder1", TextNode.valueOf("value1"))) - .origin("http://example.com") - .status(200) - .errors(emptyList()) - .build()); + assertThat(ampEvent.getAuctionContext().getBidResponse()).isEqualTo(expectedBidResponse); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"hb_cache_id_bidder1\":\"value1\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldReturnSendAmpEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of("exitpoint-module", "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(givenBidResponse(mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(), + null)))); + + // when + target.handle(routingContext); + + // then + final AmpEvent ampEvent = captureAmpEvent(); + final BidResponse bidResponse = ampEvent.getBidResponse(); + final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList( + ExtModulesTraceAnalyticsActivity.of( + "some-activity", + "success", + singletonList(ExtModulesTraceAnalyticsResult.of( + "success", + mapper.createObjectNode(), + givenExtModulesTraceAnalyticsAppliedTo()))))); + assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of( + 8L, + List.of( + ExtModulesTraceStage.of( + Stage.auction_response, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "auction-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + asList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook2") + .action(ExecutionAction.no_action) + .build())))))), + + ExtModulesTraceStage.of( + Stage.exitpoint, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "http-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + singletonList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(expectedAnalyticsTags) + .build()))))))))); + } + + @Test + public void shouldReturnSendAmpEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() { + // given + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode optionsNode = analyticsNode.putObject("options"); + optionsNode.put("enableclientdetails", true); + + final AuctionContext auctionContext = givenAuctionContext( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .analytics(analyticsNode) + .build()))).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(givenBidResponse(mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(), + null)))); + + // when + target.handle(routingContext); + + // then + final AmpEvent ampEvent = captureAmpEvent(); + final BidResponse bidResponse = ampEvent.getBidResponse(); + assertThat(bidResponse.getExt()) + .extracting(ExtBidResponse::getPrebid) + .extracting(ExtBidResponsePrebid::getAnalytics) + .extracting(ExtAnalytics::getTags) + .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class)) + .hasSize(1) + .allSatisfy(extAnalyticsTags -> { + assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint); + assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module"); + assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull(); + }); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() { + return ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes() { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .message("module1 hook2") + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); } private AuctionContext givenAuctionContext( - Function bidRequestBuilderCustomizer) { + UnaryOperator bidRequestBuilderCustomizer) { + final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(BidRequest.builder() .imp(emptyList()).tmax(5000L)).build(); return AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of(true, null, null)) + .build()) .uidsCookie(uidsCookie) .bidRequest(bidRequest) .requestTypeMetric(MetricName.amp) .timeoutContext(TimeoutContext.of(0, timeout, 0)) - .debugContext(DebugContext.empty()) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_amp)) .build(); } @@ -925,7 +1363,6 @@ private void givenHoldAuction(BidResponse bidResponse) { .willAnswer(inv -> Future.succeededFuture(((AuctionContext) inv.getArgument(0)).toBuilder() .bidResponse(bidResponse) .build())); - } private static BidResponse givenBidResponse(ObjectNode extBid) { diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index 87396cca90d..1618caea8d2 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -1,5 +1,6 @@ package org.prebid.server.handler.openrtb2; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -21,9 +22,11 @@ import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.BlocklistedAccountException; @@ -31,12 +34,28 @@ import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; @@ -44,18 +63,36 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.time.Clock; import java.time.Instant; +import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.UnaryOperator; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.function.UnaryOperator.identity; @@ -93,8 +130,12 @@ public class AuctionHandlerTest extends VertxTest { private HttpInteractionLogger httpInteractionLogger; @Mock private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; - private AuctionHandler auctionHandler; + private AuctionHandler target; @Mock private RoutingContext routingContext; @Mock @@ -118,24 +159,34 @@ public void setUp() { given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); given(httpResponse.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); - given(skippedAuctionService.skipAuction(any())).willReturn(Future.failedFuture("Auction cannot be skipped")); + given(skippedAuctionService.skipAuction(any())) + .willReturn(Future.failedFuture("Auction cannot be skipped")); given(clock.millis()).willReturn(Instant.now().toEpochMilli()); given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + timeout = new TimeoutFactory(clock).create(2000L); - auctionHandler = new AuctionHandler( + target = new AuctionHandler( 0.01, auctionRequestFactory, exchangeService, skippedAuctionService, analyticsReporterDelegator, metrics, + hooksMetricsService, clock, httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, jacksonMapper); } @@ -150,7 +201,7 @@ public void shouldSetRequestTypeMetricToAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionContext auctionContext = captureAuctionContext(); @@ -168,7 +219,7 @@ public void shouldUseTimeoutFromAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -194,7 +245,7 @@ public void shouldAddPrebidVersionResponseHeader() { .build())); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -218,7 +269,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() { .build())); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -240,7 +291,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -260,13 +311,14 @@ public void shouldRespondWithServiceUnavailableIfBidRequestHasAccountBlocklisted .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(403)); verify(httpResponse).end(eq("Blocklisted: Blocklisted account")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_account)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -278,13 +330,14 @@ public void shouldRespondWithBadRequestIfBidRequestHasAccountWithInvalidConfig() .willReturn(Future.failedFuture(new InvalidAccountConfigException("Invalid config"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Invalid config")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.bad_requests)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -296,13 +349,14 @@ public void shouldRespondWithServiceUnavailableIfBidRequestHasAppBlocklisted() { .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(403)); verify(httpResponse).end(eq("Blocklisted: Blocklisted app")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_app)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -314,13 +368,14 @@ public void shouldRespondWithBadRequestIfBidRequestIsInvalid() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Invalid request format: Request is invalid")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -332,12 +387,13 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); verify(httpResponse).setStatusCode(eq(401)); verify(httpResponse).end(eq("Account id is not provided")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -352,13 +408,14 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { .willThrow(new RuntimeException("Unexpected exception")); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -372,28 +429,26 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test public void shouldRespondWithBidResponse() { // given + final AuctionContext auctionContext = givenAuctionContext(identity()); given(auctionRequestFactory.parseRequest(any(), anyLong())) - .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + .willReturn(Future.succeededFuture(auctionContext)); given(auctionRequestFactory.enrichAuctionContext(any())) .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); - - final AuctionContext auctionContext = AuctionContext.builder() - .bidResponse(BidResponse.builder().build()) - .build(); given(exchangeService.holdAuction(any())) - .willReturn(Future.succeededFuture(auctionContext)); + .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build()))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(exchangeService).holdAuction(any()); @@ -404,13 +459,70 @@ public void shouldRespondWithBidResponse() { tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("{}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithBidResponseWhenExitpointChangesHeadersAndResponse() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(exchangeService.holdAuction(any())) + .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build()))); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"response\":{}}")))); + + // when + target.handle(routingContext); + + // then + verify(exchangeService).holdAuction(any()); + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value")); + + verify(httpResponse).end(eq("{\"response\":{}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() { // given + final AuctionContext auctionContext = givenAuctionContext(identity()); given(auctionRequestFactory.parseRequest(any(), anyLong())) - .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + .willReturn(Future.succeededFuture(auctionContext)); given(auctionRequestFactory.enrichAuctionContext(any())) .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); @@ -430,20 +542,26 @@ public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() { .debug(ExtResponseDebug.of(null, resolvedRequest, null)) .build()) .build(); - final AuctionContext auctionContext = AuctionContext.builder() - .bidResponse(bidResponse) - .build(); given(exchangeService.holdAuction(any())) - .willReturn(Future.succeededFuture(auctionContext)); + .willReturn(Future.succeededFuture(auctionContext.with(bidResponse))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(exchangeService).holdAuction(any()); verify(httpResponse).end(eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":" + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":" + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}")); + + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":" + + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":" + + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}"), + any()); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -457,7 +575,7 @@ public void shouldIncrementOkOpenrtb2WebRequestMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok)); @@ -475,7 +593,7 @@ public void shouldIncrementOkOpenrtb2AppRequestMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2app), eq(MetricName.ok)); @@ -492,7 +610,7 @@ public void shouldIncrementAppRequestMetrics() { .willReturn(Future.succeededFuture(givenAuctionContext(builder -> builder.app(App.builder().build())))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); @@ -514,7 +632,7 @@ public void shouldIncrementNoCookieMetrics() { + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7"); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); @@ -532,7 +650,7 @@ public void shouldIncrementImpsRequestedMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); @@ -551,7 +669,7 @@ public void shouldIncrementImpTypesMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateImpTypesMetrics(same(imps)); @@ -564,7 +682,7 @@ public void shouldIncrementBadinputOnParsingRequestOpenrtb2WebRequestMetrics() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput)); @@ -577,7 +695,7 @@ public void shouldIncrementErrOpenrtb2WebRequestMetrics() { .willReturn(Future.failedFuture(new RuntimeException())); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err)); @@ -587,7 +705,6 @@ public void shouldIncrementErrOpenrtb2WebRequestMetrics() { @Test public void shouldUpdateRequestTimeMetric() { // given - // set up clock mock to check that request_time metric has been updated with expected value given(clock.millis()).willReturn(5000L).willReturn(5500L); @@ -605,7 +722,7 @@ public void shouldUpdateRequestTimeMetric() { }); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); @@ -618,7 +735,7 @@ public void shouldNotUpdateRequestTimeMetricIfRequestFails() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).endHandler(any()); @@ -642,7 +759,7 @@ public void shouldUpdateNetworkErrorMetric() { }); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); @@ -659,7 +776,7 @@ public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); @@ -678,7 +795,7 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); @@ -691,7 +808,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -700,6 +817,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .status(400) .errors(singletonList("Invalid request format: Request is invalid")) .build()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -715,7 +833,7 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .willThrow(new RuntimeException("Unexpected exception")); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -729,6 +847,8 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .status(500) .errors(singletonList("Unexpected exception")) .build()); + + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -743,22 +863,71 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); - final AuctionContext expectedAuctionContext = auctionContext.toBuilder() - .requestTypeMetric(MetricName.openrtb2web) - .bidResponse(BidResponse.builder().build()) - .build(); + assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext()); + assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build()); + assertThat(auctionEvent.getStatus()).isEqualTo(200); + assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); + assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build()); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); - assertThat(auctionEvent).isEqualTo(AuctionEvent.builder() - .httpContext(givenHttpContext()) - .auctionContext(expectedAuctionContext) - .bidResponse(BidResponse.builder().build()) - .status(200) - .errors(emptyList()) - .build()); + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"response\":{}}")))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext()); + assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build()); + assertThat(auctionEvent.getStatus()).isEqualTo(200); + assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); + assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build()); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -775,7 +944,7 @@ public void shouldTolerateDuplicateQueryParamNames() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -799,7 +968,7 @@ public void shouldTolerateDuplicateHeaderNames() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -821,17 +990,236 @@ public void shouldSkipAuction() { givenAuctionContext.skipAuction().with(BidResponse.builder().build()))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(auctionRequestFactory, never()).enrichAuctionContext(any()); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok)); - verifyNoInteractions(exchangeService); - verifyNoInteractions(analyticsReporterDelegator); + verifyNoInteractions(exchangeService, analyticsReporterDelegator, hookStageExecutor); + verify(hooksMetricsService).updateHooksMetrics(any()); verify(httpResponse).setStatusCode(eq(200)); verify(httpResponse).end("{}"); } + @Test + public void shouldReturnSendAuctionEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final BidResponse bidResponse = auctionEvent.getBidResponse(); + final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList( + ExtModulesTraceAnalyticsActivity.of( + "some-activity", + "success", + singletonList(ExtModulesTraceAnalyticsResult.of( + "success", + mapper.createObjectNode(), + givenExtModulesTraceAnalyticsAppliedTo()))))); + assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of( + 8L, + List.of( + ExtModulesTraceStage.of( + Stage.auction_response, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "auction-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + asList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook2") + .action(ExecutionAction.no_action) + .build())))))), + + ExtModulesTraceStage.of( + Stage.exitpoint, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "http-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + singletonList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(expectedAnalyticsTags) + .build()))))))))); + } + + @Test + public void shouldReturnSendAuctionEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() { + // given + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode optionsNode = analyticsNode.putObject("options"); + optionsNode.put("enableclientdetails", true); + + final AuctionContext givenAuctionContext = givenAuctionContext( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .analytics(analyticsNode) + .build()))).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final BidResponse bidResponse = auctionEvent.getBidResponse(); + assertThat(bidResponse.getExt()) + .extracting(ExtBidResponse::getPrebid) + .extracting(ExtBidResponsePrebid::getAnalytics) + .extracting(ExtAnalytics::getTags) + .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class)) + .hasSize(1) + .allSatisfy(extAnalyticsTags -> { + assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint); + assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module"); + assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull(); + }); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() { + return ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes() { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .message("module1 hook2") + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); + } + private AuctionContext captureAuctionContext() { final ArgumentCaptor captor = ArgumentCaptor.forClass(AuctionContext.class); verify(exchangeService).holdAuction(captor.capture()); @@ -863,9 +1251,14 @@ private AuctionContext givenAuctionContext( .imp(emptyList())).build(); final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of(true, null, null)) + .build()) .uidsCookie(uidsCookie) .bidRequest(bidRequest) .requestTypeMetric(MetricName.openrtb2web) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_auction)) .timeoutContext(TimeoutContext.of(0, timeout, 0)); return auctionContextCustomizer.apply(auctionContextBuilder) diff --git a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java index fe59fb31fcc..64efc34c093 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java @@ -19,19 +19,27 @@ import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.CachedDebugLog; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.WithPodErrors; +import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.cache.CoreCacheService; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; import org.prebid.server.proto.response.VideoResponse; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; @@ -56,6 +64,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -86,8 +95,12 @@ public class VideoHandlerTest extends VertxTest { private UidsCookie uidsCookie; @Mock private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; - private VideoHandler videoHandler; + private VideoHandler target; private Timeout timeout; @@ -107,16 +120,25 @@ public void setUp() { given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + timeout = new TimeoutFactory(clock).create(2000L); - videoHandler = new VideoHandler( + target = new VideoHandler( videoRequestFactory, videoResponseFactory, exchangeService, coreCacheService, analyticsReporterDelegator, metrics, + hooksMetricsService, clock, prebidVersionProvider, + hookStageExecutor, jacksonMapper); } @@ -130,7 +152,7 @@ public void shouldUseTimeoutFromAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -154,7 +176,7 @@ public void shouldAddPrebidVersionResponseHeader() { .build())); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -176,7 +198,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() { .build())); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -196,7 +218,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -214,11 +236,12 @@ public void shouldRespondWithBadRequestIfBidRequestIsInvalid() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Invalid request format: Request is invalid")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -228,12 +251,13 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", "1"))); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); verify(httpResponse).setStatusCode(eq(401)); verify(httpResponse).end(eq("Unauthorised: Account id is not provided")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -246,11 +270,12 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { .willThrow(new RuntimeException("Unexpected exception")); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -262,10 +287,11 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -280,10 +306,10 @@ public void shouldRespondWithBidResponse() { .willReturn(VideoResponse.of(emptyList(), null)); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then - verify(videoResponseFactory).toVideoResponse(any(), any(), any()); + verify(videoResponseFactory, times(2)).toVideoResponse(any(), any(), any()); assertThat(httpResponse.headers()).hasSize(2) .extracting(Map.Entry::getKey, Map.Entry::getValue) @@ -291,6 +317,63 @@ public void shouldRespondWithBidResponse() { tuple("Content-Type", "application/json"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("{\"adPods\":[]}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"adPods\":[]}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithBidResponseWhenExitpointHookChangesResponseAndHeaders() { + // given + given(videoRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity(), emptyList()))); + + givenHoldAuction(BidResponse.builder().build()); + + given(videoResponseFactory.toVideoResponse(any(), any(), any())) + .willReturn(VideoResponse.of(emptyList(), null)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"adPods\":[{\"something\":1}]}")))); + + // when + target.handle(routingContext); + + // then + verify(videoResponseFactory, times(2)).toVideoResponse(any(), any(), any()); + + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value")); + verify(httpResponse).end(eq("{\"adPods\":[{\"something\":1}]}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"adPods\":[]}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -309,7 +392,7 @@ public void shouldUpdateVideoEventWithCacheLogIdErrorAndCallCacheForDebugLogWhen given(coreCacheService.cacheVideoDebugLog(any(), anyInt())).willReturn("cacheKey"); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(coreCacheService).cacheVideoDebugLog(any(), anyInt()); @@ -327,6 +410,7 @@ public void shouldCacheDebugLogWhenNoBidsWereReturnedAndDoesNotAddErrorToVideoEv final AuctionContext auctionContext = AuctionContext.builder() .bidRequest(BidRequest.builder().imp(emptyList()).build()) .account(Account.builder().auction(AccountAuctionConfig.builder().videoCacheTtl(100).build()).build()) + .debugContext(DebugContext.empty()) .cachedDebugLog(cachedDebugLog) .build(); @@ -343,7 +427,7 @@ public void shouldCacheDebugLogWhenNoBidsWereReturnedAndDoesNotAddErrorToVideoEv .willReturn(VideoResponse.of(emptyList(), null)); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(coreCacheService).cacheVideoDebugLog(any(), anyInt()); @@ -377,6 +461,8 @@ private WithPodErrors givenAuctionContext( .uidsCookie(uidsCookie) .bidRequest(bidRequest) .timeoutContext(TimeoutContext.of(0, timeout, 0)) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_video)) .build(); return WithPodErrors.of(auctionContext, errors); diff --git a/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java b/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java index e7b8bff269e..b46abafac7e 100644 --- a/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java +++ b/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java @@ -9,7 +9,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.health.model.StatusResponse; diff --git a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java index 108605efe58..330e779a304 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.StageWithHookType; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; @@ -19,6 +20,7 @@ import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -41,23 +43,17 @@ public void setUp() { } @Test - public void hookByIdShouldTolerateUnknownModule() { - // when - final EntrypointHook foundHook = hookCatalog.hookById( - "unknown-module", null, StageWithHookType.ENTRYPOINT); - - // then - assertThat(foundHook).isNull(); + public void hookByIdShouldThrowExceptionOnUnknownModule() { + // when and then + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> + hookCatalog.hookById(HookId.of("unknown-module", null), StageWithHookType.ENTRYPOINT)); } @Test - public void hookByIdShouldTolerateUnknownHook() { - // when - final EntrypointHook foundHook = hookCatalog.hookById( - "sample-module", "unknown-hook", StageWithHookType.ENTRYPOINT); - - // then - assertThat(foundHook).isNull(); + public void hookByIdShouldThrowExceptionOnUnknownHook() { + // when and then + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> + hookCatalog.hookById(HookId.of("sample-module", "unknown-hook"), StageWithHookType.ENTRYPOINT)); } @Test @@ -67,7 +63,7 @@ public void hookByIdShouldReturnEntrypointHook() { // when final EntrypointHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.ENTRYPOINT); + HookId.of("sample-module", "sample-hook"), StageWithHookType.ENTRYPOINT); // then assertThat(foundHook).isNotNull() @@ -82,7 +78,7 @@ public void hookByIdShouldReturnRawAuctionRequestHook() { // when final RawAuctionRequestHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.RAW_AUCTION_REQUEST); + HookId.of("sample-module", "sample-hook"), StageWithHookType.RAW_AUCTION_REQUEST); // then assertThat(foundHook).isNotNull() @@ -97,7 +93,7 @@ public void hookByIdShouldReturnProcessedAuctionRequestHook() { // when final ProcessedAuctionRequestHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.PROCESSED_AUCTION_REQUEST); + HookId.of("sample-module", "sample-hook"), StageWithHookType.PROCESSED_AUCTION_REQUEST); // then assertThat(foundHook).isNotNull() @@ -112,7 +108,7 @@ public void hookByIdShouldReturnBidderRequestHook() { // when final BidderRequestHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.BIDDER_REQUEST); + HookId.of("sample-module", "sample-hook"), StageWithHookType.BIDDER_REQUEST); // then assertThat(foundHook).isNotNull() @@ -127,7 +123,7 @@ public void hookByIdShouldReturnRawBidderResponseHook() { // when final RawBidderResponseHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.RAW_BIDDER_RESPONSE); + HookId.of("sample-module", "sample-hook"), StageWithHookType.RAW_BIDDER_RESPONSE); // then assertThat(foundHook).isNotNull() @@ -142,7 +138,7 @@ public void hookByIdShouldReturnProcessedBidderResponseHook() { // when final ProcessedBidderResponseHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.PROCESSED_BIDDER_RESPONSE); + HookId.of("sample-module", "sample-hook"), StageWithHookType.PROCESSED_BIDDER_RESPONSE); // then assertThat(foundHook).isNotNull() @@ -157,7 +153,7 @@ public void hookByIdShouldReturnAuctionResponseHook() { // when final AuctionResponseHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.AUCTION_RESPONSE); + HookId.of("sample-module", "sample-hook"), StageWithHookType.AUCTION_RESPONSE); // then assertThat(foundHook).isNotNull() diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java index 060855334cc..7f1d29925c1 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; -import io.vertx.core.CompositeFuture; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.junit5.Checkpoint; @@ -19,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; @@ -28,7 +31,8 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ABTest; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionAction; import org.prebid.server.hooks.execution.model.ExecutionGroup; @@ -43,21 +47,23 @@ import org.prebid.server.hooks.execution.model.StageExecutionOutcome; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.AppliedToImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; @@ -74,14 +80,18 @@ import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.Endpoint; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountHooksConfiguration; +import org.prebid.server.settings.model.HooksAdminConfig; import java.time.Clock; import java.time.ZoneOffset; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -92,13 +102,14 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; @@ -149,10 +160,10 @@ public void creationShouldFailWhenHostExecutionPlanHasUnknownHook() { HookId.of("module-alpha", "hook-a"), HookId.of("module-beta", "hook-a"))))))))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) - .willReturn(null); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willThrow(new IllegalArgumentException("Exception.")); - givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction())); + givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction())); assertThatThrownBy(() -> createExecutor(hostPlan)) .isInstanceOf(IllegalArgumentException.class) @@ -172,10 +183,10 @@ public void creationShouldFailWhenDefaultAccountExecutionPlanHasUnknownHook() { HookId.of("module-alpha", "hook-a"), HookId.of("module-beta", "hook-a"))))))))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) - .willReturn(null); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willThrow(new IllegalArgumentException("Exception.")); - givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction())); + givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction())); assertThatThrownBy(() -> createExecutor(null, defaultAccountPlan)) .isInstanceOf(IllegalArgumentException.class) @@ -233,7 +244,7 @@ public void shouldExecuteEntrypointHooksHappyPath(VertxTestContext context) { givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded( + immediateHook(InvocationResultUtils.succeeded( payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-abc"), "moduleAlphaContext"))); @@ -241,19 +252,19 @@ public void shouldExecuteEntrypointHooksHappyPath(VertxTestContext context) { givenEntrypointHook( "module-alpha", "hook-b", - delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-def")), 40)); givenEntrypointHook( "module-beta", "hook-a", - delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 80)); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded( + immediateHook(InvocationResultUtils.succeeded( payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl"), "moduleBetaContext"))); @@ -401,6 +412,70 @@ public void shouldBypassEntrypointHooksWhenNoPlanForStage(VertxTestContext conte })); } + @Test + public void shouldBypassEntrypointHooksThatAreDisabled(VertxTestContext context) { + // given + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded( + payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-abc"), + "moduleAlphaContext"))); + + givenEntrypointHook( + "module-alpha", + "hook-b", + delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-def")), 40)); + + givenEntrypointHook( + "module-beta", + "hook-a", + delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 80)); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded( + payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-jkl"), + "moduleBetaContext"))); + + final HookStageExecutor executor = HookStageExecutor.create( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach())))), + null, + Map.of("module-alpha", false), + hookCatalog, + timeoutFactory, + vertx, + clock, + jacksonMapper, + false); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.isShouldReject()).isFalse(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.body()).isEqualTo("body-ghi-jkl")); + + context.completeNow(); + })); + } + @Test public void shouldExecuteEntrypointHooksToleratingMisbehavingHooks(VertxTestContext context) { // given @@ -427,7 +502,7 @@ public void shouldExecuteEntrypointHooksToleratingMisbehavingHooks(VertxTestCont givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookStageExecutor executor = createExecutor( @@ -523,7 +598,7 @@ public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(VertxTe "module-alpha", "hook-b", delayedHook( - InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-def")), 250)); @@ -532,14 +607,14 @@ public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(VertxTe "module-beta", "hook-a", delayedHook( - InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 250)); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); @@ -622,23 +697,23 @@ public void shouldExecuteEntrypointHooksHonoringStatusAndAction(VertxTestContext givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.failed("Failed to contact service ACME"))); + immediateHook(InvocationResultUtils.failed("Failed to contact service ACME"))); givenEntrypointHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.noAction())); + immediateHook(InvocationResultUtils.noAction())); givenEntrypointHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-ghi")))); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookStageExecutor executor = createExecutor( @@ -714,13 +789,13 @@ public void shouldExecuteEntrypointHooksWhenRequestIsRejectedByFirstGroup(VertxT givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-abc")))); givenEntrypointHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is of low quality"))); + immediateHook(InvocationResultUtils.rejected("Request is of low quality"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -786,24 +861,24 @@ public void shouldExecuteEntrypointHooksWhenRequestIsRejectedBySecondGroup(Vertx givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-abc")))); givenEntrypointHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.rejected("Request is of low quality"))); + immediateHook(InvocationResultUtils.rejected("Request is of low quality"))); givenEntrypointHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-def")))); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookStageExecutor executor = createExecutor( @@ -902,7 +977,7 @@ public void shouldExecuteEntrypointHooksToleratingMisbehavingInvocationResult(Ve givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> { + immediateHook(InvocationResultUtils.succeeded(payload -> { throw new RuntimeException("Can not alter payload"); }))); @@ -1044,14 +1119,14 @@ public void shouldExecuteEntrypointHooksAndStoreResultInExecutionContext(VertxTe public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContext context) { // given final EntrypointHookImpl hookImpl = spy( - EntrypointHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + EntrypointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1107,8 +1182,8 @@ public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContex public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(VertxTestContext context) { // given final RawAuctionRequestHookImpl hookImpl = spy( - RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction()))); - given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction()))); + given(hookCatalog.hookById(any(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final String hostPlan = executionPlan(singletonMap( @@ -1140,9 +1215,9 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert verify(hookImpl, times(2)).call(any(), any()); verify(hookCatalog, times(2)) - .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); verify(hookCatalog, times(2)) - .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); context.completeNow(); })); @@ -1152,8 +1227,8 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan(VertxTestContext context) { // given final RawAuctionRequestHookImpl hookImpl = spy( - RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction()))); - given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction()))); + given(hookCatalog.hookById(any(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final String hostPlan = executionPlan(singletonMap( @@ -1169,14 +1244,14 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan final HookStageExecutor executor = createExecutor(hostPlan, defaultAccountPlan); final BidRequest bidRequest = BidRequest.builder().build(); - final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap( + final ExecutionPlan accountPlan = ExecutionPlan.of(emptyList(), singletonMap( Endpoint.openrtb2_auction, EndpointExecutionPlan.of(singletonMap( Stage.raw_auction_request, execPlanOneGroupOneHook("module-beta", "hook-b"))))); final Account account = Account.builder() .id("accountId") - .hooks(AccountHooksConfiguration.of(accountPlan, null)) + .hooks(AccountHooksConfiguration.of(accountPlan, null, null)) .build(); final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); @@ -1196,11 +1271,11 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan verify(hookImpl, times(2)).call(any(), any()); verify(hookCatalog, times(2)) - .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); verify(hookCatalog) - .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); verify(hookCatalog) - .hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); context.completeNow(); })); @@ -1209,18 +1284,18 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan @Test public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPlan(VertxTestContext context) { // given - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) - .willReturn(null); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willThrow(new IllegalArgumentException("Hook implementation does not exist or disabled")); givenRawAuctionRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); final HookStageExecutor executor = createExecutor(null, null); - final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap( + final ExecutionPlan accountPlan = ExecutionPlan.of(emptyList(), singletonMap( Endpoint.openrtb2_auction, EndpointExecutionPlan.of(singletonMap( Stage.raw_auction_request, @@ -1232,7 +1307,7 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla HookId.of("module-beta", "hook-a"))))))))); final Account account = Account.builder() .id("accountId") - .hooks(AccountHooksConfiguration.of(accountPlan, null)) + .hooks(AccountHooksConfiguration.of(accountPlan, null, null)) .build(); final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); @@ -1287,31 +1362,264 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla })); } + @Test + public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsNotRequired(VertxTestContext context) { + // given + givenRawAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + + givenRawAuctionRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenRawAuctionRequestHook( + "module-gamma", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenRawAuctionRequestHook( + "module-delta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + givenRawAuctionRequestHook( + "module-epsilon", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().site(Site.builder().build()).build())))); + + givenRawAuctionRequestHook( + "module-zeta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().user(User.builder().build()).build())))); + + final StageExecutionPlan stageExecutionPlan = StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"), + HookId.of("module-epsilon", "hook-a"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-gamma", "hook-b"), + HookId.of("module-delta", "hook-b"), + HookId.of("module-zeta", "hook-b"))))); + + final String hostExecutionPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.raw_auction_request, stageExecutionPlan)))); + + final HookStageExecutor executor = HookStageExecutor.create( + hostExecutionPlan, + null, + Map.of("module-epsilon", true, "module-zeta", false), + hookCatalog, + timeoutFactory, + vertx, + clock, + jacksonMapper, + false); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final BidRequest givenBidRequest = BidRequest.builder().build(); + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(givenBidRequest) + .account(Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of( + null, + Map.of("module-alpha", mapper.createObjectNode(), + "module-beta", mapper.createObjectNode(), + "module-gamma", mapper.createObjectNode(), + "module-zeta", mapper.createObjectNode()), + HooksAdminConfig.builder() + .moduleExecution(Map.of( + "module-alpha", true, + "module-beta", false, + "module-epsilon", false)) + .build())) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .tmax(1000L) + .site(Site.builder().build()) + .build())); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("auction-request")); + + context.completeNow(); + })); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksWhenAccountConfigIsRequired(VertxTestContext context) { + // given + givenRawAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + + givenRawAuctionRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenRawAuctionRequestHook( + "module-gamma", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenRawAuctionRequestHook( + "module-delta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + givenRawAuctionRequestHook( + "module-epsilon", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().site(Site.builder().build()).build())))); + + givenRawAuctionRequestHook( + "module-zeta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().user(User.builder().build()).build())))); + + final StageExecutionPlan stageExecutionPlan = StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"), + HookId.of("module-epsilon", "hook-a"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-gamma", "hook-b"), + HookId.of("module-delta", "hook-b"), + HookId.of("module-zeta", "hook-b"))))); + + final String hostExecutionPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.raw_auction_request, stageExecutionPlan)))); + + final HookStageExecutor executor = HookStageExecutor.create( + hostExecutionPlan, + null, + Map.of("module-epsilon", true, "module-zeta", false), + hookCatalog, + timeoutFactory, + vertx, + clock, + jacksonMapper, + true); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final BidRequest givenBidRequest = BidRequest.builder().build(); + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(givenBidRequest) + .account(Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of( + null, + Map.of("module-alpha", mapper.createObjectNode(), + "module-beta", mapper.createObjectNode(), + "module-gamma", mapper.createObjectNode(), + "module-zeta", mapper.createObjectNode()), + HooksAdminConfig.builder() + .moduleExecution(Map.of( + "module-alpha", true, + "module-beta", false, + "module-epsilon", false)) + .build())) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .site(Site.builder().build()) + .build())); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("auction-request")); + + context.completeNow(); + })); + } + @Test public void shouldExecuteRawAuctionRequestHooksHappyPath(VertxTestContext context) { // given givenRawAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().at(1).build())))); givenRawAuctionRequestHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); givenRawAuctionRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().test(1).build())))); givenRawAuctionRequestHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); final HookStageExecutor executor = createExecutor( @@ -1359,14 +1667,14 @@ public void shouldExecuteRawAuctionRequestHooksHappyPath(VertxTestContext contex public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final RawAuctionRequestHookImpl hookImpl = spy( - RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1389,7 +1697,7 @@ public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(V AuctionContext.builder() .bidRequest(BidRequest.builder().build()) .account(Account.builder() - .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration)) + .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration, null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -1450,17 +1758,17 @@ public void shouldExecuteRawAuctionRequestHooksAndPassModuleContextBetweenHooks( .build())); return promise.future(); })); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1532,7 +1840,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenRequestIsRejected(VertxTestCo givenRawAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is no good"))); + immediateHook(InvocationResultUtils.rejected("Request is no good"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -1565,25 +1873,25 @@ public void shouldExecuteProcessedAuctionRequestHooksHappyPath(VertxTestContext givenProcessedAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().at(1).build())))); givenProcessedAuctionRequestHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); givenProcessedAuctionRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().test(1).build())))); givenProcessedAuctionRequestHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); final HookStageExecutor executor = createExecutor( @@ -1632,14 +1940,14 @@ public void shouldExecuteProcessedAuctionRequestHooksHappyPath(VertxTestContext public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final ProcessedAuctionRequestHookImpl hookImpl = spy( - ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1663,7 +1971,7 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationCon AuctionContext.builder() .bidRequest(BidRequest.builder().build()) .account(Account.builder() - .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration)) + .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration, null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -1724,17 +2032,17 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassModuleContextBetween .build())); return promise.future(); })); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1807,7 +2115,7 @@ public void shouldExecuteProcessedAuctionRequestHooksWhenRequestIsRejected(Vertx givenProcessedAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is no good"))); + immediateHook(InvocationResultUtils.rejected("Request is no good"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -1843,25 +2151,25 @@ public void shouldExecuteBidderRequestHooksHappyPath(VertxTestContext context) { givenBidderRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().at(1).build())))); givenBidderRequestHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); givenBidderRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().test(1).build())))); givenBidderRequestHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); final HookStageExecutor executor = createExecutor( @@ -1928,8 +2236,8 @@ public void shouldExecuteBidderRequestHooksHappyPath(VertxTestContext context) { public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxTestContext context) { // given final BidderRequestHookImpl hookImpl = spy( - BidderRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.BIDDER_REQUEST))) + BidderRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.BIDDER_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1950,7 +2258,7 @@ public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxT .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -1979,7 +2287,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().id("bidId").build(), @@ -1990,7 +2298,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adid("adId").build(), @@ -2001,7 +2309,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().cid("cid").build(), @@ -2012,7 +2320,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adm("adm").build(), @@ -2066,7 +2374,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex checkpoint1.flag(); })); - CompositeFuture.join(future1, future2).onComplete(context.succeeding(result -> { + Future.join(future1, future2).onComplete(context.succeeding(result -> { assertThat(hookExecutionContext.getStageOutcomes()) .hasEntrySatisfying( Stage.raw_bidder_response, @@ -2083,8 +2391,8 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(VertxTestContext context) { // given final RawBidderResponseHookImpl hookImpl = spy( - RawBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) + RawBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2105,7 +2413,7 @@ public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(Ve .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2134,7 +2442,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().id("bidId").build(), @@ -2145,7 +2453,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adid("adId").build(), @@ -2156,7 +2464,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().cid("cid").build(), @@ -2167,7 +2475,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adm("adm").build(), @@ -2241,8 +2549,8 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationContext(VertxTestContext context) { // given final ProcessedBidderResponseHookImpl hookImpl = spy( - ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) + ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2266,7 +2574,7 @@ public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationCont .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2305,7 +2613,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2317,7 +2625,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2329,7 +2637,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2341,7 +2649,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2406,8 +2714,8 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final AllProcessedBidResponsesHookImpl hookImpl = spy( - AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) + AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2431,7 +2739,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationCo .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2459,7 +2767,7 @@ public void shouldExecuteBidderRequestHooksWhenRequestIsRejected(VertxTestContex givenBidderRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is no good"))); + immediateHook(InvocationResultUtils.rejected("Request is no good"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -2495,25 +2803,25 @@ public void shouldExecuteAuctionResponseHooksHappyPath(VertxTestContext context) givenAuctionResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().id("id").build())))); givenAuctionResponseHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().bidid("bidid").build())))); givenAuctionResponseHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().cur("cur").build())))); givenAuctionResponseHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().nbr(1).build())))); final HookStageExecutor executor = createExecutor( @@ -2554,8 +2862,8 @@ public void shouldExecuteAuctionResponseHooksHappyPath(VertxTestContext context) public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final AuctionResponseHookImpl hookImpl = spy( - AuctionResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) + AuctionResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2573,7 +2881,7 @@ public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(Ver .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2595,13 +2903,55 @@ null, singletonMap("module-alpha", mapper.createObjectNode()))) })); } + @Test + public void shouldExecuteAuctionResponseHooksAndTolerateNullAccount(VertxTestContext context) { + // given + final AuctionResponseHookImpl hookImpl = spy( + AuctionResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.auction_response, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeAuctionResponseStage( + BidResponse.builder().build(), + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(null) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNull(); + }); + + context.completeNow(); + })); + } + @Test public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext context) { // given givenAuctionResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Will not apply"))); + immediateHook(InvocationResultUtils.rejected("Will not apply"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -2651,8 +3001,263 @@ public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext })); } + @Test + public void shouldExecuteExitpointHooksHappyPath(VertxTestContext context) { + // given + givenExitpointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-alpha-a", "alpha-a"), + "{\"execution1\":\"alpha-a\"")))); + + givenExitpointHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-alpha-b", "alpha-b"), + payload.responseBody() + ",\"execution4\":\"alpha-b\"}")))); + + givenExitpointHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-beta-a", "beta-a"), + payload.responseBody() + ",\"execution2\":\"beta-a\"")))); + + givenExitpointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-beta-b", "beta-b"), + payload.responseBody() + ",\"execution3\":\"beta-b\"")))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.exitpoint, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeExitpointStage( + MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"), + "{}", + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> { + assertThat(payload.responseBody()) + .isEqualTo("{\"execution1\":\"alpha-a\",\"execution2\":\"beta-a\"," + + "\"execution3\":\"beta-b\",\"execution4\":\"alpha-b\"}"); + assertThat(payload.responseHeaders()).hasSize(5) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Header-Name", "Header-Value"), + tuple("Header-alpha-a", "alpha-a"), + tuple("Header-alpha-b", "alpha-b"), + tuple("Header-beta-a", "beta-a"), + tuple("Header-beta-b", "beta-b")); + }); + + context.completeNow(); + })); + } + + @Test + public void shouldExecuteExitpointHooksAndPassAuctionInvocationContext(VertxTestContext context) { + // given + final ExitpointHookImpl hookImpl = spy( + ExitpointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.EXITPOINT))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.exitpoint, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeExitpointStage( + MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"), + "{}", + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of( + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNotNull(); + }); + + context.completeNow(); + })); + } + + @Test + public void shouldExecuteExitpointHooksAndIgnoreRejection(VertxTestContext context) { + // given + givenExitpointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.rejected("Will not apply"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.exitpoint, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeExitpointStage( + MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"), + "{}", + AuctionContext.builder() + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result.isShouldReject()).isFalse(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> { + assertThat(payload.responseBody()).isNotNull(); + assertThat(payload.responseBody()).isNotEmpty(); + }); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.exitpoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .allSatisfy(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-response"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.getFirst().getHooks(); + assertThat(group0Hooks.getFirst()).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.execution_failure); + assertThat(hookOutcome.getMessage()) + .isEqualTo("Rejection is not supported during this stage"); + }); + })); + + context.completeNow(); + })); + } + + @Test + public void abTestsForEntrypointStageShouldReturnEnabledTests() { + // given + final HookStageExecutor executor = createExecutor(executionPlan(asList( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("2")).build(), + ABTest.builder().enabled(true).build()))); + + // when + final List abTests = executor.abTestsForEntrypointStage(); + + // then + assertThat(abTests) + .hasSize(2) + .extracting(ABTest::isEnabled) + .containsOnly(true); + } + + @Test + public void abTestsShouldReturnEnabledTestsFromAccount() { + // given + final HookStageExecutor executor = createExecutor(executionPlan(asList( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("2")).build(), + ABTest.builder().enabled(true).build()))); + + final Account account = Account.builder() + .id("1") + .hooks(AccountHooksConfiguration.of( + ExecutionPlan.of( + asList( + ABTest.builder().enabled(true).accounts(singleton("3")).build(), + ABTest.builder().enabled(false).accounts(singleton("4")).build(), + ABTest.builder().enabled(true).build()), + emptyMap()), + emptyMap(), + null)) + .build(); + + // when + final List abTests = executor.abTests(account); + + // then + assertThat(abTests).containsExactly( + ABTest.builder().enabled(true).accounts(singleton("3")).build(), + ABTest.builder().enabled(true).build()); + } + + @Test + public void abTestsShouldReturnEnabledTestsFromHost() { + // given + final HookStageExecutor executor = createExecutor( + executionPlan(asList( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("2")).build(), + ABTest.builder().enabled(true).build())), + jacksonMapper.encodeToString(ExecutionPlan.empty())); + + final Account account = Account.builder() + .id("1") + .build(); + + // when + final List abTests = executor.abTests(account); + + // then + assertThat(abTests).containsExactly( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(true).build()); + } + private String executionPlan(Map endpoints) { - return jacksonMapper.encodeToString(ExecutionPlan.of(endpoints)); + return jacksonMapper.encodeToString(ExecutionPlan.of(null, endpoints)); + } + + private String executionPlan(List abTests) { + return jacksonMapper.encodeToString(ExecutionPlan.of(abTests, emptyMap())); } private static StageExecutionPlan execPlanTwoGroupsTwoHooksEach() { @@ -2681,7 +3286,7 @@ private void givenEntrypointHook( String hookImplCode, BiFunction>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.ENTRYPOINT))) .willReturn(EntrypointHookImpl.of(delegate)); } @@ -2693,7 +3298,7 @@ private void givenRawAuctionRequestHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(RawAuctionRequestHookImpl.of(delegate)); } @@ -2705,7 +3310,7 @@ private void givenProcessedAuctionRequestHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(ProcessedAuctionRequestHookImpl.of(delegate)); } @@ -2717,7 +3322,7 @@ private void givenBidderRequestHook( BidderInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.BIDDER_REQUEST))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.BIDDER_REQUEST))) .willReturn(BidderRequestHookImpl.of(delegate)); } @@ -2729,7 +3334,7 @@ private void givenRawBidderResponseHook( BidderInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) .willReturn(RawBidderResponseHookImpl.of(delegate)); } @@ -2741,7 +3346,7 @@ private void givenProcessedBidderResponseHook( BidderInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) .willReturn(ProcessedBidderResponseHookImpl.of(delegate)); } @@ -2753,7 +3358,7 @@ private void givenAllProcessedBidderResponsesHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) .willReturn(AllProcessedBidResponsesHookImpl.of(delegate)); } @@ -2765,10 +3370,22 @@ private void givenAuctionResponseHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE))) .willReturn(AuctionResponseHookImpl.of(delegate)); } + private void givenExitpointHook( + String moduleCode, + String hookImplCode, + BiFunction< + ExitpointPayload, + AuctionInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.EXITPOINT))) + .willReturn(ExitpointHookImpl.of(delegate)); + } + private BiFunction>> delayedHook( InvocationResult result, int delay) { @@ -2786,6 +3403,10 @@ private BiFunction Future.succeededFuture(result); } + private static HookId eqHook(String moduleCode, String hookCode) { + return ArgumentMatchers.eq(HookId.of(moduleCode, hookCode)); + } + private HookStageExecutor createExecutor(String hostExecutionPlan) { return createExecutor(hostExecutionPlan, null); } @@ -2794,11 +3415,13 @@ private HookStageExecutor createExecutor(String hostExecutionPlan, String defaul return HookStageExecutor.create( hostExecutionPlan, defaultAccountExecutionPlan, + Collections.emptyMap(), hookCatalog, timeoutFactory, vertx, clock, - jacksonMapper); + jacksonMapper, + false); } @Value(staticConstructor = "of") @@ -2990,4 +3613,28 @@ public String code() { return code; } } + + @Value(staticConstructor = "of") + @NonFinal + private static class ExitpointHookImpl implements ExitpointHook { + + String code = "hook-code"; + + BiFunction< + ExitpointPayload, + AuctionInvocationContext, + Future>> delegate; + + @Override + public Future> call(ExitpointPayload payload, + AuctionInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } } diff --git a/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java new file mode 100644 index 00000000000..fa6f3291267 --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java @@ -0,0 +1,132 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.VertxTest; +import org.prebid.server.hooks.execution.model.ABTest; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.model.Endpoint; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ABTestHookProviderTest extends VertxTest { + + @Mock + private HookProvider innerHookProvider; + + @Mock + private Hook innerHook; + + @BeforeEach + public void setUp() { + given(innerHookProvider.apply(any())).willReturn(innerHook); + } + + @Test + public void applyShouldReturnOriginalHookIfNoABTestFound() { + // given + final HookProvider target = new ABTestHookProvider<>( + innerHookProvider, + singletonList(ABTest.builder().moduleCode("otherModule").build()), + HookExecutionContext.of(Endpoint.openrtb2_auction), + mapper); + + // when + final Hook result = target.apply(hookId()); + + // then + verify(innerHookProvider).apply(any()); + verifyNoInteractions(innerHook); + assertThat(result).isSameAs(innerHook); + } + + @Test + public void applyShouldReturnWrappedHook() { + // given + final HookProvider target = new ABTestHookProvider<>( + innerHookProvider, + singletonList(ABTest.builder().moduleCode("module").build()), + HookExecutionContext.of(Endpoint.openrtb2_auction), + mapper); + + // when + final Hook result = target.apply(hookId()); + + // then + verify(innerHookProvider).apply(any()); + verifyNoInteractions(innerHook); + assertThat(result).isInstanceOf(ABTestHook.class); + } + + @Test + public void shouldInvokeHookShouldReturnTrueIfThereIsAPreviousInvocation() { + // given + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + hookExecutionContext.getStageOutcomes().put(Stage.entrypoint, singletonList( + StageExecutionOutcome.of("entity", singletonList(GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(hookId()) + .action(ExecutionAction.update) + .build())))))); + + final ABTestHookProvider target = new ABTestHookProvider<>( + innerHookProvider, + emptyList(), + hookExecutionContext, + mapper); + + // when and then + verifyNoInteractions(innerHookProvider); + verifyNoInteractions(innerHook); + assertThat(target.shouldInvokeHook("module", null)).isTrue(); + } + + @Test + public void shouldInvokeHookShouldReturnFalseIfThereIsAPreviousExecutionWithoutInvocation() { + // given + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + hookExecutionContext.getStageOutcomes().put(Stage.entrypoint, singletonList( + StageExecutionOutcome.of("entity", singletonList(GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(hookId()) + .action(ExecutionAction.no_invocation) + .build())))))); + + final ABTestHookProvider target = new ABTestHookProvider<>( + innerHookProvider, + emptyList(), + hookExecutionContext, + mapper); + + // when and then + verifyNoInteractions(innerHookProvider); + verifyNoInteractions(innerHook); + assertThat(target.shouldInvokeHook("module", null)).isFalse(); + } + + private static HookId hookId() { + return HookId.of("module", "hook"); + } +} diff --git a/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java new file mode 100644 index 00000000000..f0d129cb5db --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java @@ -0,0 +1,183 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import io.vertx.core.Future; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultUtils; +import org.prebid.server.hooks.v1.InvocationStatus; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.hooks.v1.PayloadUpdate.identity; + +@ExtendWith(MockitoExtension.class) +public class ABTestHookTest extends VertxTest { + + @Mock + private Hook innerHook; + + @Mock + private Object payload; + + @Mock + private InvocationContext invocationContext; + + @Test + public void codeShouldReturnSameHookCode() { + // given + given(innerHook.code()).willReturn("code"); + + final Hook target = new ABTestHook<>( + "module", + innerHook, + false, + false, + mapper); + + // when and then + assertThat(target.code()).isEqualTo("code"); + } + + @Test + public void callShouldReturnSkippedResultWithoutTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + false, + false, + mapper); + + // when + final InvocationResult invocationResult = target.call(payload, invocationContext).result(); + + // then + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation); + assertThat(invocationResult.analyticsTags()).isNull(); + } + + @Test + public void callShouldReturnSkippedResultWithTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + false, + true, + mapper); + + // when + final InvocationResult invocationResult = target.call(payload, invocationContext).result(); + + // then + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation); + assertThat(invocationResult.analyticsTags().activities()).hasSize(1).allSatisfy(activity -> { + assertThat(activity.name()).isEqualTo("core-module-abtests"); + assertThat(activity.status()).isEqualTo("success"); + assertThat(activity.results()).hasSize(1).allSatisfy(result -> { + assertThat(result.status()).isEqualTo("skipped"); + assertThat(result.values()).isEqualTo(mapper.createObjectNode().put("module", "module")); + }); + }); + } + + @Test + public void callShouldReturnRunResultWithoutTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + true, + false, + mapper); + + final InvocationResult innerHookInvocationResult = spy(InvocationResultUtils.succeeded(identity())); + final Future> innerHookResult = Future.succeededFuture(innerHookInvocationResult); + given(innerHook.call(any(), any())).willReturn(innerHookResult); + + // when + final Future> result = target.call(payload, invocationContext); + + // then + verify(innerHook).call(same(payload), same(invocationContext)); + verifyNoInteractions(innerHookInvocationResult); + assertThat(result).isSameAs(innerHookResult); + } + + @Test + public void callShouldReturnRunResultWithTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + true, + true, + mapper); + + final InvocationResult innerHookInvocationResult = spy(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .message("message") + .action(InvocationAction.update) + .payloadUpdate(identity()) + .errors(singletonList("error")) + .warnings(singletonList("warning")) + .debugMessages(singletonList("debugMessages")) + .moduleContext(new Object()) + .analyticsTags(TagsImpl.of(asList( + ActivityImpl.of("activity0", null, null), + ActivityImpl.of("activity1", null, null)))) + .build()); + given(innerHook.call(any(), any())).willReturn(Future.succeededFuture(innerHookInvocationResult)); + + // when + final Future> result = target.call(payload, invocationContext); + + // then + verify(innerHook).call(same(payload), same(invocationContext)); + verifyNoInteractions(innerHookInvocationResult); + + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult.status()).isSameAs(innerHookInvocationResult.status()); + assertThat(invocationResult.message()).isSameAs(innerHookInvocationResult.message()); + assertThat(invocationResult.action()).isSameAs(innerHookInvocationResult.action()); + assertThat(invocationResult.payloadUpdate()).isSameAs(innerHookInvocationResult.payloadUpdate()); + assertThat(invocationResult.errors()).isSameAs(innerHookInvocationResult.errors()); + assertThat(invocationResult.warnings()).isSameAs(innerHookInvocationResult.warnings()); + assertThat(invocationResult.debugMessages()).isSameAs(innerHookInvocationResult.debugMessages()); + assertThat(invocationResult.moduleContext()).isSameAs(innerHookInvocationResult.moduleContext()); + assertThat(invocationResult.analyticsTags().activities()).satisfies(activities -> { + for (int i = 0; i < activities.size() - 1; i++) { + assertThat(activities.get(i)).isSameAs(innerHookInvocationResult.analyticsTags().activities().get(i)); + } + + assertThat(activities.getLast()).satisfies(activity -> { + assertThat(activity.name()).isEqualTo("core-module-abtests"); + assertThat(activity.status()).isEqualTo("success"); + assertThat(activity.results()).hasSize(1).allSatisfy(activityResult -> { + assertThat(activityResult.status()).isEqualTo("run"); + assertThat(activityResult.values()) + .isEqualTo(mapper.createObjectNode().put("module", "module")); + }); + }); + }); + } +} diff --git a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java similarity index 75% rename from src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java rename to src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java index 31426173e9c..71d21ce9596 100644 --- a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java +++ b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java @@ -1,34 +1,12 @@ package org.prebid.server.hooks.v1; -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; -import java.util.List; +public class InvocationResultUtils { -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { + private InvocationResultUtils() { - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; + } public static InvocationResult succeeded(PayloadUpdate payloadUpdate) { return InvocationResultImpl.builder() diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java deleted file mode 100644 index 0965bef2b40..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java deleted file mode 100644 index 810313936a8..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java deleted file mode 100644 index 3558a22c3cd..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java deleted file mode 100644 index 92278f2469c..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/src/test/java/org/prebid/server/it/AdnuntiusTest.java b/src/test/java/org/prebid/server/it/AdnuntiusTest.java index 23bb6000371..9a03d73ccf5 100644 --- a/src/test/java/org/prebid/server/it/AdnuntiusTest.java +++ b/src/test/java/org/prebid/server/it/AdnuntiusTest.java @@ -18,7 +18,6 @@ public class AdnuntiusTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdnuntius() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adnuntius-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adnuntius/test-adnuntius-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adnuntius/test-adnuntius-bid-response.json")))); diff --git a/src/test/java/org/prebid/server/it/AdtelligentTest.java b/src/test/java/org/prebid/server/it/AdtelligentTest.java index c46f87e83b8..b1122d4908d 100644 --- a/src/test/java/org/prebid/server/it/AdtelligentTest.java +++ b/src/test/java/org/prebid/server/it/AdtelligentTest.java @@ -19,9 +19,9 @@ public class AdtelligentTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromAdtelligent() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtelligent-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-request-1.json"))) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-response-1.json")))); + jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-response.json")))); // when final Response response = responseFor("openrtb2/adtelligent/test-auction-adtelligent-request.json", diff --git a/src/test/java/org/prebid/server/it/AdtonosTest.java b/src/test/java/org/prebid/server/it/AdtonosTest.java new file mode 100644 index 00000000000..389edc02a5e --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdtonosTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class AdtonosTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheAdtonosBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtonos-exchange/testPublisherId")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/adtonos/test-adtonos-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/adtonos/test-adtonos-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adtonos/test-auction-adtonos-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adtonos/test-auction-adtonos-response.json", response, + singletonList("adtonos")); + } +} diff --git a/src/test/java/org/prebid/server/it/BidmaticTest.java b/src/test/java/org/prebid/server/it/BidmaticTest.java new file mode 100644 index 00000000000..b05011b5656 --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidmaticTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class BidmaticTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBidmatic() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidmatic-exchange")) + .withQueryParam("source", equalTo("1000")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bidmatic/test-bidmatic-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bidmatic/test-bidmatic-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidmatic/test-auction-bidmatic-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bidmatic/test-auction-bidmatic-response.json", response, + singletonList("bidmatic")); + } +} diff --git a/src/test/java/org/prebid/server/it/BizzclickTest.java b/src/test/java/org/prebid/server/it/BlastoTest.java similarity index 57% rename from src/test/java/org/prebid/server/it/BizzclickTest.java rename to src/test/java/org/prebid/server/it/BlastoTest.java index 2ef4c68dffa..e26d75e6ca1 100644 --- a/src/test/java/org/prebid/server/it/BizzclickTest.java +++ b/src/test/java/org/prebid/server/it/BlastoTest.java @@ -14,24 +14,23 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static java.util.Collections.singletonList; -public class BizzclickTest extends IntegrationTest { +public class BlastoTest extends IntegrationTest { @Test - public void openrtb2AuctionShouldRespondWithBidsFromBizzclick() throws IOException, JSONException { + public void openrtb2AuctionShouldRespondWithBidsFromBlasto() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bizzclick-exchange")) - .withQueryParam("host", equalTo("host")) - .withQueryParam("source", equalTo("placementId")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/blasto-exchange")) + .withQueryParam("source", equalTo("sourceId")) .withQueryParam("account", equalTo("accountId")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/bizzclick/test-bizzclick-bid-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/bizzclick/test-bizzclick-bid-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/blasto/test-blasto-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/blasto/test-blasto-bid-response.json")))); // when - final Response response = responseFor("openrtb2/bizzclick/test-auction-bizzclick-request.json", + final Response response = responseFor("openrtb2/blasto/test-auction-blasto-request.json", Endpoint.openrtb2_auction); // then - assertJsonEquals("openrtb2/bizzclick/test-auction-bizzclick-response.json", response, - singletonList("bizzclick")); + assertJsonEquals("openrtb2/blasto/test-auction-blasto-response.json", response, + singletonList("blasto")); } } diff --git a/src/test/java/org/prebid/server/it/Copper6SspTest.java b/src/test/java/org/prebid/server/it/Copper6SspTest.java new file mode 100644 index 00000000000..dad5da9c05b --- /dev/null +++ b/src/test/java/org/prebid/server/it/Copper6SspTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class Copper6SspTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromCopper6Ssp() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/copper6ssp-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/copper6ssp/test-copper6ssp-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/copper6ssp/test-copper6ssp-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/copper6ssp/test-auction-copper6ssp-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/copper6ssp/test-auction-copper6ssp-response.json", response, + singletonList("copper6ssp")); + } +} diff --git a/src/test/java/org/prebid/server/it/EscalaxTest.java b/src/test/java/org/prebid/server/it/EscalaxTest.java new file mode 100644 index 00000000000..30831d991e5 --- /dev/null +++ b/src/test/java/org/prebid/server/it/EscalaxTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class EscalaxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromEscalax() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/escalax-exchange")) + .withQueryParam("k", equalTo("testAccountId")) + .withQueryParam("name", equalTo("testSourceId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/escalax/test-escalax-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/escalax/test-escalax-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/escalax/test-auction-escalax-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/escalax/test-auction-escalax-response.json", response, + singletonList("escalax")); + } +} diff --git a/src/test/java/org/prebid/server/it/FelixadsTest.java b/src/test/java/org/prebid/server/it/FelixadsTest.java new file mode 100644 index 00000000000..30e90dd0ba5 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FelixadsTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class FelixadsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromFelixads() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/felixads-exchange")) + .withQueryParam("host", equalTo("someUniquePartnerName")) + .withQueryParam("accountId", equalTo("someSeat")) + .withQueryParam("sourceId", equalTo("someToken")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/felixads/test-felixads-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/felixads/test-felixads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/felixads/test-auction-felixads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/felixads/test-auction-felixads-response.json", response, singletonList("felixads")); + } +} diff --git a/src/test/java/org/prebid/server/it/FilmzieTest.java b/src/test/java/org/prebid/server/it/FilmzieTest.java new file mode 100644 index 00000000000..fefbffb6e74 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FilmzieTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class FilmzieTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheFilmzieBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/filmzie-exchange/test.host/123456")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/filmzie/test-filmzie-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/filmzie/test-filmzie-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/filmzie/test-auction-filmzie-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/filmzie/test-auction-filmzie-response.json", response, + singletonList("filmzie")); + } +} diff --git a/src/test/java/org/prebid/server/it/LiftoffTest.java b/src/test/java/org/prebid/server/it/LiftoffTest.java index 83ce56922c0..921ce6806f2 100644 --- a/src/test/java/org/prebid/server/it/LiftoffTest.java +++ b/src/test/java/org/prebid/server/it/LiftoffTest.java @@ -13,7 +13,7 @@ public class LiftoffTest extends IntegrationTest { @Test - public void openrtb2AuctionShouldRespondWithBidsFromliftoff() throws IOException, JSONException { + public void openrtb2AuctionShouldRespondWithBidsFromLiftoff() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/liftoff-exchange")) .withRequestBody(WireMock.equalToJson( diff --git a/src/test/java/org/prebid/server/it/MagniteTest.java b/src/test/java/org/prebid/server/it/MagniteTest.java index 6623ca7d9a9..c02387d55dc 100644 --- a/src/test/java/org/prebid/server/it/MagniteTest.java +++ b/src/test/java/org/prebid/server/it/MagniteTest.java @@ -4,6 +4,8 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.prebid.server.model.Endpoint; +import org.prebid.server.version.PrebidVersionProvider; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -15,11 +17,15 @@ public class MagniteTest extends IntegrationTest { + @Autowired + private PrebidVersionProvider versionProvider; + @Test public void testOpenrtb2AuctionCoreFunctionality() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/magnite-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/magnite/test-magnite-bid-request.json"))) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/magnite/test-magnite-bid-request.json", versionProvider))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/magnite/test-magnite-bid-response.json")))); // when diff --git a/src/test/java/org/prebid/server/it/MeloZenTest.java b/src/test/java/org/prebid/server/it/MeloZenTest.java new file mode 100644 index 00000000000..185aa894a2f --- /dev/null +++ b/src/test/java/org/prebid/server/it/MeloZenTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class MeloZenTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMelozen() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/melozen-exchange")) + .withQueryParam("pubId", equalTo("publisherId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/melozen/test-melozen-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/melozen/test-melozen-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/melozen/test-auction-melozen-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals("openrtb2/melozen/test-auction-melozen-response.json", response, List.of("melozen")); + } + +} diff --git a/src/test/java/org/prebid/server/it/MetaxTest.java b/src/test/java/org/prebid/server/it/MetaxTest.java new file mode 100644 index 00000000000..f51a55e55c2 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MetaxTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class MetaxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMetax() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/metax-exchange")) + .withQueryParam("publisher_id", equalTo("123")) + .withQueryParam("adunit", equalTo("456")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/metax/test-metax-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/metax/test-metax-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/metax/test-auction-metax-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/metax/test-auction-metax-response.json", response, + singletonList("metax")); + } +} diff --git a/src/test/java/org/prebid/server/it/MinuteMediaTest.java b/src/test/java/org/prebid/server/it/MinuteMediaTest.java index 0aace384eb5..2f8c591dc08 100644 --- a/src/test/java/org/prebid/server/it/MinuteMediaTest.java +++ b/src/test/java/org/prebid/server/it/MinuteMediaTest.java @@ -19,7 +19,6 @@ public class MinuteMediaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromMinuteMedia() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/minutemedia-exchange")) .withQueryParam("publisherId", equalTo("123")) .withRequestBody(equalToJson(jsonFrom("openrtb2/minutemedia/test-minutemedia-bid-request.json"))) diff --git a/src/test/java/org/prebid/server/it/MissenaTest.java b/src/test/java/org/prebid/server/it/MissenaTest.java new file mode 100644 index 00000000000..e86227fb358 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MissenaTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class MissenaTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMissena() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/missena-exchange")) + .withQueryParam("t", equalTo("apiKey")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/missena/test-missena-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/missena/test-missena-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/missena/test-auction-missena-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/missena/test-auction-missena-response.json", response, + singletonList("missena")); + } +} diff --git a/src/test/java/org/prebid/server/it/OrakiTest.java b/src/test/java/org/prebid/server/it/OrakiTest.java new file mode 100644 index 00000000000..ff7b8880c32 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OrakiTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class OrakiTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOraki() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/oraki-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/oraki/test-oraki-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/oraki/test-oraki-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/oraki/test-auction-oraki-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/oraki/test-auction-oraki-response.json", response, + singletonList("oraki")); + } +} diff --git a/src/test/java/org/prebid/server/it/OwnAdxTest.java b/src/test/java/org/prebid/server/it/OwnAdxTest.java new file mode 100644 index 00000000000..1ab4ddb5d71 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OwnAdxTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class OwnAdxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOwnAdx() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ownadx-exchange/bid/testSeatId/testSspId")) + .withQueryParam("token", equalTo("testTokenId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/ownadx/test-ownadx-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/ownadx/test-ownadx-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/ownadx/test-auction-ownadx-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/ownadx/test-auction-ownadx-response.json", response, singletonList("ownadx")); + } +} diff --git a/src/test/java/org/prebid/server/it/PrecisoTest.java b/src/test/java/org/prebid/server/it/PrecisoTest.java index 5d97978e7b7..da461016e64 100644 --- a/src/test/java/org/prebid/server/it/PrecisoTest.java +++ b/src/test/java/org/prebid/server/it/PrecisoTest.java @@ -23,8 +23,8 @@ public void openrtb2AuctionShouldRespondWithBidsFromPreciso() throws IOException "openrtb2/preciso/test-preciso-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/preciso/test-preciso-bid-response.json")))); - // when + // when final Response response = responseFor("openrtb2/preciso/test-auction-preciso-request.json", Endpoint.openrtb2_auction); diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java index d18446e7834..3dd300524c0 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -1,13 +1,19 @@ package org.prebid.server.it; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.prebid.server.model.Endpoint; import org.prebid.server.util.IntegrationTestsUtil; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; import java.io.IOException; @@ -17,15 +23,42 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; - -@TestPropertySource(properties = {"price-floors.enabled=true", "server.http.port=8050", "admin.port=0"}) -public class PriceFloorsTest extends IntegrationTest { +import static org.prebid.server.util.IntegrationTestsUtil.assertJsonEquals; +import static org.prebid.server.util.IntegrationTestsUtil.jsonFrom; +import static org.prebid.server.util.IntegrationTestsUtil.responseFor; + +// TODO: Investigate the root cause of unstable behavior in this class and remove the disabled state once resolved. +@Disabled +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@TestPropertySource( + locations = {"test-application.properties"}, + properties = { + "price-floors.enabled=true", + "server.http.port=8050", + "admin.port=0", + "settings.in-memory-cache.http-update.endpoint=http://localhost:8100/periodic-update", + "settings.in-memory-cache.http-update.amp-endpoint=http://localhost:8100/periodic-update", + "currency-converter.external-rates.url=http://localhost:8100/currency-rates", + "adapters.generic.endpoint=http://localhost:8100/generic-exchange" + }) +public class PriceFloorsTest { private static final int APP_PORT = 8050; - private static final int WIREMOCK_PORT = 8090; + private static final int WIREMOCK_PORT = 8100; + + @SuppressWarnings("unchecked") + @RegisterExtension + public static final WireMockExtension WIRE_MOCK_RULE = WireMockExtension.newInstance() + .options(wireMockConfig() + .port(WIREMOCK_PORT) + .gzipDisabled(true) + .jettyStopTimeout(5000L) + .extensions(IntegrationTest.CacheResponseTransformer.class)) + .build(); private static final String PRICE_FLOORS = "Price Floors Test"; private static final String FLOORS_FROM_REQUEST = "Floors from request"; @@ -34,13 +67,23 @@ public class PriceFloorsTest extends IntegrationTest { private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); @BeforeAll - public static void setUpJunk() throws IOException { + public static void beforeAll() throws IOException { WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/periodic-update")) .willReturn(aResponse().withBody(jsonFrom("storedrequests/test-periodic-refresh.json")))); WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/currency-rates")) .willReturn(aResponse().withBody(jsonFrom("currency/latest.json")))); } + @BeforeEach + public void setUp() throws IOException { + beforeAll(); + } + + @AfterEach + public void resetWireMock() { + WIRE_MOCK_RULE.resetAll(); + } + @Test public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IOException, JSONException { // given @@ -55,13 +98,13 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO .willSetStateTo(FLOORS_FROM_REQUEST)); // when - final Response firstResponse = IntegrationTestsUtil.responseFor( + final Response firstResponse = responseFor( "openrtb2/floors/floors-test-auction-request-1.json", Endpoint.openrtb2_auction, SPEC); // then - IntegrationTestsUtil.assertJsonEquals( + assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", firstResponse, singletonList("generic"), @@ -76,14 +119,14 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO .willSetStateTo(FLOORS_FROM_PROVIDER)); // when - final Response secondResponse = IntegrationTestsUtil.responseFor( + final Response secondResponse = responseFor( "openrtb2/floors/floors-test-auction-request-2.json", Endpoint.openrtb2_auction, SPEC); // then assertThat(stubMapping.getNewScenarioState()).isEqualTo(FLOORS_FROM_PROVIDER); - IntegrationTestsUtil.assertJsonEquals( + assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", secondResponse, singletonList("generic"), @@ -99,13 +142,13 @@ public void openrtb2AuctionShouldSkipPriceFloorsForTheGenericBidderWhenGenericIs .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/floors-test-bid-response.json")))); // when - final Response firstResponse = IntegrationTestsUtil.responseFor( + final Response firstResponse = responseFor( "openrtb2/floors/floors-test-auction-request-no-signal.json", Endpoint.openrtb2_auction, SPEC); // then - IntegrationTestsUtil.assertJsonEquals( + assertJsonEquals( "openrtb2/floors/floors-test-auction-response-no-signal.json", firstResponse, singletonList("generic"), diff --git a/src/test/java/org/prebid/server/it/PubriseTest.java b/src/test/java/org/prebid/server/it/PubriseTest.java new file mode 100644 index 00000000000..c5ab3812e24 --- /dev/null +++ b/src/test/java/org/prebid/server/it/PubriseTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class PubriseTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromPubrise() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubrise-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/pubrise/test-pubrise-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubrise/test-pubrise-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/pubrise/test-auction-pubrise-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/pubrise/test-auction-pubrise-response.json", response, + singletonList("pubrise")); + } +} diff --git a/src/test/java/org/prebid/server/it/QtTest.java b/src/test/java/org/prebid/server/it/QtTest.java new file mode 100644 index 00000000000..eff861a5434 --- /dev/null +++ b/src/test/java/org/prebid/server/it/QtTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class QtTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromQt() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/qt-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/qt/test-qt-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/qt/test-qt-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/qt/test-auction-qt-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/qt/test-auction-qt-response.json", response, + singletonList("qt")); + } +} diff --git a/src/test/java/org/prebid/server/it/RubiconTest.java b/src/test/java/org/prebid/server/it/RubiconTest.java index 85f521abb80..d7799bc50eb 100644 --- a/src/test/java/org/prebid/server/it/RubiconTest.java +++ b/src/test/java/org/prebid/server/it/RubiconTest.java @@ -4,6 +4,8 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.prebid.server.model.Endpoint; +import org.prebid.server.version.PrebidVersionProvider; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -15,11 +17,15 @@ public class RubiconTest extends IntegrationTest { + @Autowired + PrebidVersionProvider versionProvider; + @Test public void testOpenrtb2AuctionCoreFunctionality() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon/test-rubicon-bid-request.json"))) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon/test-rubicon-bid-request.json", versionProvider))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/rubicon/test-rubicon-bid-response.json")))); // when diff --git a/src/test/java/org/prebid/server/it/StreamlynTest.java b/src/test/java/org/prebid/server/it/StreamlynTest.java new file mode 100644 index 00000000000..b482c4d2192 --- /dev/null +++ b/src/test/java/org/prebid/server/it/StreamlynTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class StreamlynTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheStreamlynBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/streamlyn-exchange/test.host/123456")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/streamlyn/test-streamlyn-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/streamlyn/test-streamlyn-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/streamlyn/test-auction-streamlyn-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/streamlyn/test-auction-streamlyn-response.json", response, + singletonList("streamlyn")); + } +} diff --git a/src/test/java/org/prebid/server/it/TgmTest.java b/src/test/java/org/prebid/server/it/TgmTest.java new file mode 100644 index 00000000000..e94e25abe08 --- /dev/null +++ b/src/test/java/org/prebid/server/it/TgmTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class TgmTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheTgmBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/tgm-exchange/test.host/123456")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/tgm/test-tgm-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/tgm/test-tgm-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/tgm/test-auction-tgm-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/tgm/test-auction-tgm-response.json", response, + singletonList("tgm")); + } +} diff --git a/src/test/java/org/prebid/server/it/TheTradeDeskTest.java b/src/test/java/org/prebid/server/it/TheTradeDeskTest.java new file mode 100644 index 00000000000..f2ea877239f --- /dev/null +++ b/src/test/java/org/prebid/server/it/TheTradeDeskTest.java @@ -0,0 +1,40 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class TheTradeDeskTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheTradeDesk() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/thetradedesk-exchange/somesupplyid")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/thetradedesk/test-thetradedesk-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/thetradedesk/test-thetradedesk-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/thetradedesk/test-auction-thetradedesk-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals( + "openrtb2/thetradedesk/test-auction-thetradedesk-response.json", + response, + List.of("thetradedesk")); + } + +} diff --git a/src/test/java/org/prebid/server/it/TradPlusTest.java b/src/test/java/org/prebid/server/it/TradPlusTest.java new file mode 100644 index 00000000000..894bc3e4da3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/TradPlusTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class TradPlusTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTradPlus() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/accountTestID/tradplus-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/tradplus/test-tradplus-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/tradplus/test-tradplus-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/tradplus/test-auction-tradplus-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/tradplus/test-auction-tradplus-response.json", response, singletonList("tradplus")); + } +} diff --git a/src/test/java/org/prebid/server/it/VimayxTest.java b/src/test/java/org/prebid/server/it/VimayxTest.java new file mode 100644 index 00000000000..2995e51d1da --- /dev/null +++ b/src/test/java/org/prebid/server/it/VimayxTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class VimayxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromVimayx() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/vimayx-exchange")) + .withQueryParam("host", equalTo("someUniquePartnerName")) + .withQueryParam("accountId", equalTo("someSeat")) + .withQueryParam("sourceId", equalTo("someToken")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/vimayx/test-vimayx-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/vimayx/test-vimayx-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/vimayx/test-auction-vimayx-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/vimayx/test-auction-vimayx-response.json", response, singletonList("vimayx")); + } +} + diff --git a/src/test/java/org/prebid/server/it/VungleTest.java b/src/test/java/org/prebid/server/it/VungleTest.java new file mode 100644 index 00000000000..9cdd8511ce3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/VungleTest.java @@ -0,0 +1,31 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.client.WireMock; +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static java.util.Collections.singletonList; + +public class VungleTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromVungle() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/vungle-exchange")) + .withRequestBody(WireMock.equalToJson( + jsonFrom("openrtb2/vungle/test-vungle-bid-request.json"))) + .willReturn(WireMock.aResponse().withBody( + jsonFrom("openrtb2/vungle/test-vungle-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/vungle/test-auction-vungle-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/vungle/test-auction-vungle-response.json", response, singletonList("vungle")); + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/HooksTest.java b/src/test/java/org/prebid/server/it/hooks/HooksTest.java index 21664060409..902f12c6589 100644 --- a/src/test/java/org/prebid/server/it/hooks/HooksTest.java +++ b/src/test/java/org/prebid/server/it/hooks/HooksTest.java @@ -1,13 +1,29 @@ package org.prebid.server.it.hooks; +import io.restassured.http.Header; import io.restassured.response.Response; import org.json.JSONException; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.it.IntegrationTest; +import org.prebid.server.version.PrebidVersionProvider; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; +import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; @@ -16,17 +32,28 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.hamcrest.Matchers.empty; public class HooksTest extends IntegrationTest { private static final String RUBICON = "rubicon"; + @Autowired + private PrebidVersionProvider versionProvider; + + @Autowired + private AnalyticsReporterDelegator analyticsReporterDelegator; + @Test public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONException { + Mockito.reset(analyticsReporterDelegator); + // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("hooks/sample-module/test-rubicon-bid-request-1.json"))) + .withRequestBody(equalToJson( + jsonFrom("hooks/sample-module/test-rubicon-bid-request-1.json", versionProvider))) .willReturn(aResponse().withBody(jsonFrom("hooks/sample-module/test-rubicon-bid-response-1.json")))); // when @@ -41,6 +68,39 @@ public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONE "hooks/sample-module/test-auction-sample-module-response.json", response, singletonList(RUBICON)); JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); + + //todo: remove everything below after at least one exitpoint module is added and tested by functional tests + assertThat(response.getHeaders()) + .extracting(Header::getName, Header::getValue) + .contains(tuple("Exitpoint-Hook-Header", "Exitpoint-Hook-Value")); + + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + Mockito.verify(analyticsReporterDelegator).processEvent(eventCaptor.capture(), Mockito.any()); + + final AuctionEvent actualEvent = eventCaptor.getValue(); + final List exitpointHookOutcomes = actualEvent.getAuctionContext() + .getHookExecutionContext().getStageOutcomes().get(Stage.exitpoint); + + final TagsImpl expectedTags = TagsImpl.of(singletonList(ActivityImpl.of( + "exitpoint-device-id", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode().put("exitpoint-some-field", "exitpoint-some-value"), + AppliedToImpl.builder() + .impIds(singletonList("impId1")) + .request(true) + .build()))))); + + assertThat(exitpointHookOutcomes).isNotEmpty().hasSize(1).first() + .extracting(StageExecutionOutcome::getGroups) + .extracting(List::getFirst) + .extracting(GroupExecutionOutcome::getHooks) + .extracting(List::getFirst) + .extracting(HookExecutionOutcome::getAnalyticsTags) + .isEqualTo(expectedTags); + + Mockito.reset(analyticsReporterDelegator); } @Test @@ -113,7 +173,8 @@ public void openrtb2AuctionShouldRejectRubiconBidderByRawBidderResponseHook() th JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); WIRE_MOCK_RULE.verify(1, postRequestedFor(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("hooks/reject/test-rubicon-bid-request-1.json")))); + .withRequestBody(equalToJson( + jsonFrom("hooks/reject/test-rubicon-bid-request-1.json", versionProvider)))); } @Test @@ -139,6 +200,7 @@ public void openrtb2AuctionShouldRejectRubiconBidderByProcessedBidderResponseHoo JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); WIRE_MOCK_RULE.verify(1, postRequestedFor(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("hooks/reject/test-rubicon-bid-request-1.json")))); + .withRequestBody(equalToJson( + jsonFrom("hooks/reject/test-rubicon-bid-request-1.json", versionProvider)))); } } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java index 360e61fae47..8073a5edc27 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java @@ -4,7 +4,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; @@ -19,7 +19,7 @@ public Future> call( final BidResponse updatedBidResponse = updateBidResponse(originalBidResponse); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of(payload.bidResponse().toBuilder() .seatbid(updatedBidResponse.getSeatbid()) .build()))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java index 10af73e4d4f..4c95bcb5a7f 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java @@ -5,7 +5,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; @@ -22,7 +22,7 @@ public Future> call( final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of(payload.bidRequest().toBuilder() .imp(updatedBidRequest.getImp()) .build()))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java index 36827bd39df..a4011db9106 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java @@ -5,7 +5,7 @@ import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -18,7 +18,7 @@ public Future> call( final boolean rejectFlag = Boolean.parseBoolean(entrypointPayload.queryParams().get("sample-it-module-reject")); if (rejectFlag) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by sample entrypoint hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by sample entrypoint hook")); } return maybeUpdate(entrypointPayload); @@ -35,7 +35,7 @@ private Future> maybeUpdate(EntrypointPayloa ? updateBody(entrypointPayload.body()) : entrypointPayload.body(); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), updatedHeaders, updatedBody))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java new file mode 100644 index 00000000000..82a494e7158 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java @@ -0,0 +1,80 @@ +package org.prebid.server.it.hooks; + +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; +import org.prebid.server.json.JacksonMapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SampleItExitpointHook implements ExitpointHook { + + private final JacksonMapper mapper; + + public SampleItExitpointHook(JacksonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Future> call(ExitpointPayload exitpointPayload, + AuctionInvocationContext invocationContext) { + + final BidResponse bidResponse = invocationContext.auctionContext().getBidResponse(); + final List seatBids = updateBids(bidResponse.getSeatbid()); + final BidResponse updatedResponse = bidResponse.toBuilder().seatbid(seatBids).build(); + + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payload -> ExitpointPayloadImpl.of( + exitpointPayload.responseHeaders().add("Exitpoint-Hook-Header", "Exitpoint-Hook-Value"), + mapper.encodeToString(updatedResponse))) + .debugMessages(Arrays.asList( + "exitpoint debug message 1", + "exitpoint debug message 2")) + .analyticsTags(TagsImpl.of(Collections.singletonList(ActivityImpl.of( + "exitpoint-device-id", + "success", + Collections.singletonList(ResultImpl.of( + "success", + mapper.mapper().createObjectNode().put("exitpoint-some-field", "exitpoint-some-value"), + AppliedToImpl.builder() + .impIds(Collections.singletonList("impId1")) + .request(true) + .build())))))) + .build()); + } + + private List updateBids(List seatBids) { + return seatBids.stream() + .map(seatBid -> seatBid.toBuilder().bid(seatBid.getBid().stream() + .map(bid -> bid.toBuilder() + .adm(bid.getAdm() + + "" + + "") + .build()) + .toList()) + .build()) + .toList(); + } + + @Override + public String code() { + return "exitpoint"; + } + +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java index e2806f8c87f..441240e7a32 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java @@ -31,7 +31,8 @@ public SampleItModule(JacksonMapper mapper) { new SampleItRejectingProcessedAuctionRequestHook(), new SampleItRejectingBidderRequestHook(), new SampleItRejectingRawBidderResponseHook(), - new SampleItRejectingProcessedBidderResponseHook()); + new SampleItRejectingProcessedBidderResponseHook(), + new SampleItExitpointHook(mapper)); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java index a285235f420..dca19dd6043 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java @@ -6,7 +6,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; @@ -29,7 +29,7 @@ public Future> call( final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(payload.bidRequest().toBuilder() .ext(updatedBidRequest.getExt()) .build()))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java index 3f8e9ee7ae2..b626e03d5ac 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java @@ -4,7 +4,7 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; @@ -21,7 +21,7 @@ public Future> call( final List updatedBids = updateBids(originalBids); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(updatedBids))); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java index c1054166dc1..f843083b0fb 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java @@ -4,15 +4,15 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.AppliedToImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java index fb6d915717e..0f30527519b 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java @@ -4,7 +4,7 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; @@ -21,7 +21,7 @@ public Future> call( final List updatedBids = updateBids(originalBids); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(updatedBids))); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java index bd90a974936..d08303b93ce 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; @@ -13,7 +13,7 @@ public class SampleItRejectingBidderRequestHook implements BidderRequestHook { public Future> call( BidderRequestPayload bidderRequestPayload, BidderInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting bidder request hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting bidder request hook")); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java index b5feb3aaef9..5dfad73d026 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; @@ -13,7 +13,7 @@ public class SampleItRejectingProcessedAuctionRequestHook implements ProcessedAu public Future> call( AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected( + return Future.succeededFuture(InvocationResultUtils.rejected( "Rejected by rejecting processed auction request hook")); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java index a6f1438402c..d2c568837ca 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; @@ -13,7 +13,7 @@ public class SampleItRejectingProcessedBidderResponseHook implements ProcessedBi public Future> call( BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected( + return Future.succeededFuture(InvocationResultUtils.rejected( "Rejected by rejecting processed bidder response hook")); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java index 5532962afc2..d4eda0346ca 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; @@ -13,7 +13,7 @@ public class SampleItRejectingRawAuctionRequestHook implements RawAuctionRequest public Future> call( AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw auction request hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting raw auction request hook")); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java index 0eeeee4375c..f2964a9871a 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; @@ -13,7 +13,7 @@ public class SampleItRejectingRawBidderResponseHook implements RawBidderResponse public Future> call( BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw bidder response hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting raw bidder response hook")); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java index a08845f9c19..5fdf0724015 100644 --- a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java +++ b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java @@ -1,9 +1,12 @@ package org.prebid.server.it.hooks; +import org.mockito.Mockito; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.hooks.v1.Module; import org.prebid.server.json.JacksonMapper; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; @TestConfiguration public class TestHooksConfiguration { @@ -12,4 +15,10 @@ public class TestHooksConfiguration { Module sampleItModule(JacksonMapper mapper) { return new SampleItModule(mapper); } + + @Bean + @Primary + AnalyticsReporterDelegator spyAnalyticsReporterDelegator(AnalyticsReporterDelegator analyticsReporterDelegator) { + return Mockito.spy(analyticsReporterDelegator); + } } diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 5594cad0c65..47b1da61b37 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -331,6 +331,16 @@ public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics() assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(4); } + @Test + public void updateDebugRequestsMetricsShouldIncrementMetrics() { + // when + metrics.updateDebugRequestMetrics(false); + metrics.updateDebugRequestMetrics(true); + + // then + assertThat(metricRegistry.counter("debug_requests").getCount()).isOne(); + } + @Test public void updateImpTypesMetricsByCountPerMediaTypeShouldIncrementMetrics() { // given @@ -427,6 +437,16 @@ public void updateAccountRequestMetricsShouldIncrementMetrics() { assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne(); } + @Test + public void updateAccountDebugRequestMetricsShouldIncrementMetrics() { + // when + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true); + + // then + assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isOne(); + } + @Test public void updateAdapterRequestTypeAndNoCookieMetricsShouldUpdateMetricsAsExpected() { @@ -916,6 +936,8 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() { given(accountMetricsVerbosityResolver.forAccount(any())).willReturn(AccountMetricsVerbosityLevel.none); // when + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true); metrics.updateAccountRequestMetrics(Account.empty(ACCOUNT_ID), MetricName.openrtb2web); metrics.updateAdapterResponseTime(RUBICON, Account.empty(ACCOUNT_ID), 500); metrics.updateAdapterRequestNobidMetrics(RUBICON, Account.empty(ACCOUNT_ID)); @@ -924,6 +946,7 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() { // then assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); @@ -939,6 +962,8 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() { // when metrics.updateAccountRequestMetrics(Account.empty(ACCOUNT_ID), MetricName.openrtb2web); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true); metrics.updateAdapterResponseTime(RUBICON, Account.empty(ACCOUNT_ID), 500); metrics.updateAdapterRequestNobidMetrics(RUBICON, Account.empty(ACCOUNT_ID)); metrics.updateAdapterRequestGotbidsMetrics(RUBICON, Account.empty(ACCOUNT_ID)); @@ -946,6 +971,7 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() { // then assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); @@ -1164,6 +1190,13 @@ public void updateHooksMetricsShouldIncrementMetrics() { "module1", Stage.entrypoint, "hook1", ExecutionStatus.success, 5L, ExecutionAction.update); metrics.updateHooksMetrics( "module1", Stage.raw_auction_request, "hook2", ExecutionStatus.success, 5L, ExecutionAction.no_action); + metrics.updateHooksMetrics( + "module1", + Stage.raw_auction_request, + "hook2", + ExecutionStatus.success, + 5L, + ExecutionAction.no_invocation); metrics.updateHooksMetrics( "module1", Stage.processed_auction_request, @@ -1176,10 +1209,13 @@ public void updateHooksMetricsShouldIncrementMetrics() { metrics.updateHooksMetrics( "module2", Stage.raw_bidder_response, "hook2", ExecutionStatus.timeout, 7L, null); metrics.updateHooksMetrics( - "module2", Stage.processed_bidder_response, "hook3", ExecutionStatus.execution_failure, 5L, null); + "module2", Stage.all_processed_bid_responses, "hook3", ExecutionStatus.execution_failure, 5L, null); metrics.updateHooksMetrics( "module2", Stage.auction_response, "hook4", ExecutionStatus.invocation_failure, 5L, null); + metrics.updateHooksMetrics( + "module1", Stage.exitpoint, "hook5", ExecutionStatus.success, 5L, ExecutionAction.update); + // then assertThat(metricRegistry.counter("modules.module.module1.stage.entrypoint.hook.hook1.call") .getCount()) @@ -1194,6 +1230,9 @@ public void updateHooksMetricsShouldIncrementMetrics() { .isEqualTo(1); assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.noop").getCount()) .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.no-invocation") + .getCount()) + .isEqualTo(1); assertThat(metricRegistry.timer("modules.module.module1.stage.rawauction.hook.hook2.duration").getCount()) .isEqualTo(1); @@ -1219,12 +1258,14 @@ public void updateHooksMetricsShouldIncrementMetrics() { assertThat(metricRegistry.timer("modules.module.module2.stage.rawbidresponse.hook.hook2.duration").getCount()) .isEqualTo(1); - assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.call").getCount()) + assertThat(metricRegistry.counter("modules.module.module2.stage.allprocbidresponses.hook.hook3.call") + .getCount()) .isEqualTo(1); - assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.execution-error") + assertThat(metricRegistry.counter("modules.module.module2.stage.allprocbidresponses.hook.hook3.execution-error") .getCount()) .isEqualTo(1); - assertThat(metricRegistry.timer("modules.module.module2.stage.procbidresponse.hook.hook3.duration").getCount()) + assertThat(metricRegistry.timer("modules.module.module2.stage.allprocbidresponses.hook.hook3.duration") + .getCount()) .isEqualTo(1); assertThat(metricRegistry.counter("modules.module.module2.stage.auctionresponse.hook.hook4.call").getCount()) @@ -1234,6 +1275,15 @@ public void updateHooksMetricsShouldIncrementMetrics() { .isEqualTo(1); assertThat(metricRegistry.timer("modules.module.module2.stage.auctionresponse.hook.hook4.duration").getCount()) .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module1.stage.exitpoint.hook.hook5.call") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.exitpoint.hook.hook5.success.update") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module1.stage.exitpoint.hook.hook5.duration").getCount()) + .isEqualTo(1); } @Test @@ -1248,6 +1298,8 @@ public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed Account.empty("accountId"), "module2", ExecutionStatus.failure, null); metrics.updateAccountHooksMetrics( Account.empty("accountId"), "module3", ExecutionStatus.timeout, null); + metrics.updateAccountHooksMetrics( + Account.empty("accountId"), "module4", ExecutionStatus.success, ExecutionAction.no_invocation); // then assertThat(metricRegistry.counter("account.accountId.modules.module.module1.call").getCount()) @@ -1264,6 +1316,11 @@ public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed .isEqualTo(1); assertThat(metricRegistry.counter("account.accountId.modules.module.module3.failure").getCount()) .isEqualTo(1); + + assertThat(metricRegistry.counter("account.accountId.modules.module.module4.call").getCount()) + .isEqualTo(0); + assertThat(metricRegistry.counter("account.accountId.modules.module.module4.success.no-invocation").getCount()) + .isEqualTo(1); } @Test diff --git a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java index 4b33f28c350..e63ad8f530d 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java @@ -85,7 +85,8 @@ public void setUp() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); } @Test @@ -99,7 +100,8 @@ public void resolveTcfContextShouldReturnContextWhenGdprIsDisabled() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); // when final Future result = target.resolveTcfContext( @@ -177,7 +179,8 @@ public void resolveTcfContextShouldCheckServiceConfigValueWhenRequestTypeIsUnkno geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final AccountGdprConfig accountGdprConfig = AccountGdprConfig.builder() .enabledForRequestType(EnabledForRequestType.of(true, true, true, true, true)) @@ -209,7 +212,8 @@ public void resolveTcfContextShouldConsiderTcfVersionOneAsCorruptedVersionTwo() geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"; @@ -227,7 +231,7 @@ public void resolveTcfContextShouldConsiderTcfVersionOneAsCorruptedVersionTwo() } @Test - public void resolveTcfContextShouldTreatTcfConsentWithTcfPolicyVersionGreaterThanFourAsCorrupted() { + public void resolveTcfContextShouldEmitWarningOnTcfConsentWithTcfPolicyVersionGreaterThanFive() { // given final GdprConfig gdprConfig = GdprConfig.builder() .enabled(true) @@ -241,7 +245,8 @@ public void resolveTcfContextShouldTreatTcfConsentWithTcfPolicyVersionGreaterTha geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = TCStringEncoder.newBuilder() .version(2) @@ -260,11 +265,13 @@ public void resolveTcfContextShouldTreatTcfConsentWithTcfPolicyVersionGreaterTha null); // then - final String expectedWarning = "Parsing consent string: %s failed. TCF policy version 6 is not supported" - .formatted(vendorConsent); + final String expectedWarning = "Unknown tcfPolicyVersion 6, defaulting to gvlSpecificationVersion=3"; assertThat(result).isSucceeded(); - assertThat(result.result().getConsent()).isInstanceOf(TCStringEmpty.class); + assertThat(result.result().getConsent()) + .extracting(TCString::getVersion, TCString::getTcfPolicyVersion) + .containsExactly(2, 6); assertThat(result.result().getWarnings()).containsExactly(expectedWarning); + verify(metrics).updateAlertsMetrics(eq(MetricName.general)); } @Test @@ -282,7 +289,8 @@ public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"; @@ -323,7 +331,8 @@ public void resolveTcfContextShouldUseEeaListFromAccountConfig() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"; @@ -442,7 +451,8 @@ public void resolveTcfContextShouldConsultDefaultValueWhenGeoLookupFailed() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); given(geoLocationServiceWrapper.doLookup(anyString(), any(), any())).willReturn(Future.failedFuture("Bad ip")); diff --git a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java index 03a538d2f58..8437698b9f0 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java @@ -11,7 +11,6 @@ import java.util.concurrent.ThreadLocalRandom; import static org.mockito.Mockito.verify; -import static org.prebid.server.assertion.FutureAssertion.assertThat; @ExtendWith(MockitoExtension.class) public class VersionedVendorListServiceTest { @@ -46,9 +45,9 @@ public void versionedVendorListServiceShouldTreatTcfPolicyLessThanFourAsVendorLi } @Test - public void versionedVendorListServiceShouldTreatTcfPolicyFourAsVendorListSpecificationThree() { + public void versionedVendorListServiceShouldTreatTcfPolicyGreaterOrEqualFourAsVendorListSpecificationThree() { // given - final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(4, 6); + final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(4, 64); final TCString consent = TCStringEncoder.newBuilder() .version(2) .tcfPolicyVersion(tcfPolicyVersion) @@ -61,20 +60,4 @@ public void versionedVendorListServiceShouldTreatTcfPolicyFourAsVendorListSpecif // then verify(vendorListServiceV3).forVersion(12); } - - @Test - public void versionedVendorListServiceShouldTreatTcfPolicyGreaterThanFourAsInvalidVersion() { - // given - final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(6, 63); - final TCString consent = TCStringEncoder.newBuilder() - .version(2) - .tcfPolicyVersion(tcfPolicyVersion) - .vendorListVersion(12) - .toTCString(); - - // when and then - assertThat(versionedVendorListService.forConsent(consent)) - .isFailed() - .hasMessage("Invalid tcf policy version: " + tcfPolicyVersion); - } } diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java index bac345c8906..1767491959a 100644 --- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java @@ -8,8 +8,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.Account; @@ -336,7 +336,6 @@ public void getCategoriesShouldNotCacheNotPreBidException() { .willReturn(Future.failedFuture(new TimeoutException("timeout"))); // when - target.getCategories("adServer", "publisher", timeout); target.getCategories("adServer", "publisher", timeout); final Future> lastFuture = diff --git a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java index bab03ea0bb8..86c72604150 100644 --- a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java @@ -8,8 +8,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java index aaa72b45756..f3180d3651e 100644 --- a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -8,7 +8,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.activity.ActivitiesConfigResolver; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JsonMerger; import org.prebid.server.settings.model.Account; diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index b2452e06cae..e3076ddbdfd 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -10,8 +10,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountPrivacyConfig; diff --git a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java new file mode 100644 index 00000000000..a702d71ab2e --- /dev/null +++ b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java @@ -0,0 +1,403 @@ +package org.prebid.server.settings; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(VertxExtension.class) +public class S3ApplicationSettingsTest extends VertxTest { + + private static final String BUCKET = "bucket"; + private static final String ACCOUNTS_DIR = "accounts"; + private static final String STORED_IMPS_DIR = "stored-imps"; + private static final String STORED_REQUESTS_DIR = "stored-requests"; + private static final String STORED_RESPONSES_DIR = "stored-responses"; + + @Mock + private S3AsyncClient s3AsyncClient; + + private Vertx vertx; + + private S3ApplicationSettings target; + + @Mock + private Timeout timeout; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new S3ApplicationSettings( + s3AsyncClient, + BUCKET, + ACCOUNTS_DIR, + STORED_IMPS_DIR, + STORED_REQUESTS_DIR, + STORED_RESPONSES_DIR, + jacksonMapper, + vertx); + + given(timeout.remaining()).willReturn(500L); + } + + @AfterEach + public void tearDown(VertxTestContext context) { + vertx.close(context.succeedingThenComplete()); + } + + @Test + public void getAccountByIdShouldReturnFetchedAccount(VertxTestContext context) throws JsonProcessingException { + // given + final Account account = Account.builder().id("accountId").build(); + + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(ACCOUNTS_DIR, "accountId")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + mapper.writeValueAsString(account).getBytes()))); + + // when + final Future result = target.getAccountById("accountId", timeout); + + // then + result.onComplete(context.succeeding(returnedAccount -> { + assertThat(returnedAccount.getId()).isEqualTo("accountId"); + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnTimeout(VertxTestContext context) { + // given + given(timeout.remaining()).willReturn(-1L); + + // when + final Future result = target.getAccountById("account", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(TimeoutException.class) + .hasMessage("Timeout has been exceeded"); + + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnAccountNotFound(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getAccountById("notFoundId", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id notFoundId not found"); + + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnInvalidJson(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "invalidJson".getBytes()))); + + // when + final Future result = target.getAccountById("invalidJsonId", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(PreBidException.class) + .hasMessage("Invalid json for account with id invalidJsonId"); + + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnAccountIdMismatch(VertxTestContext context) throws JsonProcessingException { + // given + final Account account = Account.builder().id("accountId").build(); + + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(ACCOUNTS_DIR, "anotherAccountId")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + mapper.writeValueAsString(account).getBytes()))); + + // when + final Future result = target.getAccountById("anotherAccountId", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id anotherAccountId does not match id accountId in file"); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredRequest(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_REQUESTS_DIR, "request")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedRequest".getBytes()))); + + // when + final Future result = target.getStoredData( + "accountId", Set.of("request"), emptySet(), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEqualTo(Map.of("request", "storedRequest")); + assertThat(storedDataResult.getStoredIdToImp()).isEmpty(); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredImpression(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_IMPS_DIR, "imp")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedImp".getBytes()))); + + // when + final Future result = target.getStoredData( + "accountId", emptySet(), Set.of("imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getStoredIdToImp()).isEqualTo(Map.of("imp", "storedImp")); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredImpressionWithAdUnitPath(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_IMPS_DIR, "imp")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedImp".getBytes()))); + + // when + final Future result = target.getStoredData("accountId", emptySet(), Set.of("/imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getStoredIdToImp()).isEqualTo(Map.of("/imp", "storedImp")); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredRequestAndStoredImpression(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_REQUESTS_DIR, "request")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedRequest".getBytes()))); + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_IMPS_DIR, "imp")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedImp".getBytes()))); + + // when + final Future result = target.getStoredData( + "accountId", Set.of("request"), Set.of("imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEqualTo(Map.of("request", "storedRequest")); + assertThat(storedDataResult.getStoredIdToImp()).isEqualTo(Map.of("imp", "storedImp")); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnErrorsForNotFoundRequests(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getStoredData( + "accountId", Set.of("request"), emptySet(), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToImp()).isEmpty(); + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getErrors()) + .isEqualTo(singletonList("No stored request found for id: request")); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnErrorsForNotFoundImpressions(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getStoredData( + "accountId", emptySet(), Set.of("imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToImp()).isEmpty(); + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getErrors()).isEqualTo(singletonList("No stored impression found for id: imp")); + + context.completeNow(); + })); + } + + @Test + public void getStoredResponsesShouldReturnExpectedResult(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_RESPONSES_DIR, "response1")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedResponse1".getBytes()))); + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_RESPONSES_DIR, "response2")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getStoredResponses( + Set.of("response1", "response2"), timeout); + + // then + result.onComplete(context.succeeding(storedResponseDataResult -> { + assertThat(storedResponseDataResult.getIdToStoredResponses()) + .isEqualTo(Map.of("response1", "storedResponse1")); + assertThat(storedResponseDataResult.getErrors()) + .isEqualTo(singletonList("No stored response found for id: response2")); + + context.completeNow(); + })); + } +} diff --git a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java index d0010e7699c..1e1ffd37271 100644 --- a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java @@ -10,7 +10,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.CacheNotificationListener; diff --git a/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java new file mode 100644 index 00000000000..e9b37a75d94 --- /dev/null +++ b/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java @@ -0,0 +1,174 @@ +package org.prebid.server.settings.service; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.CacheNotificationListener; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsResponse; +import software.amazon.awssdk.services.s3.model.S3Object; + +import java.time.Clock; +import java.util.concurrent.CompletableFuture; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(VertxExtension.class) +public class S3PeriodicRefreshServiceTest extends VertxTest { + + private static final String BUCKET = "bucket"; + private static final String STORED_REQ_DIR = "stored-req"; + private static final String STORED_IMP_DIR = "stored-imp"; + + @Mock(strictness = LENIENT) + private S3AsyncClient s3AsyncClient; + + @Mock + private CacheNotificationListener cacheNotificationListener; + + @Mock + private Clock clock; + + @Mock + private Metrics metrics; + + private Vertx vertx; + + @BeforeEach + public void setUp() { + vertx = spy(Vertx.vertx()); + + given(s3AsyncClient.listObjects(eq(ListObjectsRequest.builder() + .bucket(BUCKET) + .prefix(STORED_REQ_DIR) + .build()))) + .willReturn(listObjectResponse(STORED_REQ_DIR + "/id1.json")); + given(s3AsyncClient.listObjects(eq(ListObjectsRequest.builder() + .bucket(BUCKET) + .prefix(STORED_IMP_DIR) + .build()))) + .willReturn(listObjectResponse(STORED_IMP_DIR + "/id2.json")); + + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key(STORED_REQ_DIR + "/id1.json") + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(getObjectResponse("value1")); + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key(STORED_IMP_DIR + "/id2.json") + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(getObjectResponse("value2")); + + given(clock.millis()).willReturn(100L, 500L); + } + + @AfterEach + public void tearDown(VertxTestContext context) { + vertx.close(context.succeedingThenComplete()); + } + + @Test + public void initializeShouldCallSaveWithExpectedParameters(VertxTestContext context) { + // when and then + createAndInitService(100).onComplete(context.succeeding(ignored -> { + verify(cacheNotificationListener, atLeast(1)) + .save(singletonMap("id1", "value1"), singletonMap("id2", "value2")); + verify(metrics, atLeast(1)).updateSettingsCacheRefreshTime( + eq(MetricName.stored_request), eq(MetricName.initialize), eq(400L)); + + context.completeNow(); + })); + } + + @Test + public void initializeShouldNotCreatePeriodicTaskIfRefreshPeriodIsNegative(VertxTestContext context) { + // when and then + createAndInitService(-1).onComplete(context.succeeding(unused -> { + verify(vertx, never()).setPeriodic(anyLong(), any()); + + context.completeNow(); + })); + } + + @Test + public void initializeShouldUpdateMetricsOnError(VertxTestContext context) { + // given + given(s3AsyncClient.listObjects(any(ListObjectsRequest.class))) + .willReturn(CompletableFuture.failedFuture(new IllegalStateException("Failed"))); + + // when + createAndInitService(100).onComplete(context.failing(ignored -> { + verify(metrics, atLeast(1)).updateSettingsCacheRefreshTime( + eq(MetricName.stored_request), eq(MetricName.initialize), eq(400L)); + verify(metrics, atLeast(1)).updateSettingsCacheRefreshErrorMetric( + eq(MetricName.stored_request), eq(MetricName.initialize)); + + context.completeNow(); + })); + } + + private CompletableFuture listObjectResponse(String key) { + return CompletableFuture.completedFuture( + ListObjectsResponse + .builder() + .contents(singletonList(S3Object.builder().key(key).build())) + .build()); + } + + private CompletableFuture> getObjectResponse(String value) { + return CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + value.getBytes())); + } + + private Future createAndInitService(long refreshPeriod) { + final S3PeriodicRefreshService s3PeriodicRefreshService = new S3PeriodicRefreshService( + s3AsyncClient, + BUCKET, + STORED_REQ_DIR, + STORED_IMP_DIR, + refreshPeriod, + cacheNotificationListener, + MetricName.stored_request, + clock, + metrics, + vertx); + + final Promise init = Promise.promise(); + s3PeriodicRefreshService.initialize(init); + return init.future(); + } +} diff --git a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java index 1c9c609932c..a14c6141355 100644 --- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java @@ -203,7 +203,7 @@ public void validateShouldReturnValidationMessagesWhenSovrnExtNotValid() { @Test public void validateShouldNotReturnValidationMessagesWhenAdtelligentImpExtIsOk() { // given - final ExtImpAdtelligent ext = ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3)); + final ExtImpAdtelligent ext = ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3)); final JsonNode node = mapper.convertValue(ext, JsonNode.class); @@ -394,6 +394,7 @@ private static BidderInfo givenBidderInfo(String aliasOf) { null, null, 0, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/validation/ImpValidatorTest.java b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java new file mode 100644 index 00000000000..652150b6732 --- /dev/null +++ b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java @@ -0,0 +1,2395 @@ +package org.prebid.server.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Asset; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.DataObject; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.EventTracker; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.ImageObject; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Metric; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Request; +import com.iab.openrtb.request.TitleObject; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.request.VideoObject; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class ImpValidatorTest extends VertxTest { + + private static final String RUBICON = "rubicon"; + + @Mock + private BidderParamValidator bidderParamValidator; + @Mock(strictness = LENIENT) + private BidderCatalog bidderCatalog; + + private ImpValidator target; + + @BeforeEach + public void setUp() { + target = new ImpValidator(bidderParamValidator, bidderCatalog, jacksonMapper); + + given(bidderCatalog.isValidName(RUBICON)).willReturn(true); + given(bidderCatalog.isActive(RUBICON)).willReturn(true); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpIdNull() { + // given + final List givenImps = singletonList(validImpBuilder().id(null).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpIdEmptyString() { + // given + final List givenImps = singletonList(validImpBuilder().id("").build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenNoneOfMediaTypeIsPresent() { + // given + final List givenImps = singletonList(validImpBuilder() + .video(null) + .audio(null) + .banner(null) + .xNative(null) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or " + + "\"native\""); + + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenVideoAttributeIsPresentButVideoMimesMissed() { + // given + final List givenImps = singletonList(validImpBuilder() + .video(Video.builder().mimes(emptyList()).build()) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].video.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenAudioAttributePresentButAudioMimesMissed() { + // given + final List givenImps = singletonList(validImpBuilder() + .audio(Audio.builder().mimes(emptyList()).build()) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].audio.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSizes() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAndValidSizes() + throws ValidationException { + + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(250) + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSizes() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasZeroHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldNotReturnValidationMessageForSizesIfImpIsInterstitial() throws ValidationException { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .instl(1) + .banner(Banner.builder() + .w(0) + .h(300) + .format(singletonList(Format.builder().w(1).h(1).build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasZeroWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasNegativeWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasNegativeHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatHWAndRatiosPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(2).wmin(3).wratio(4).hratio(5)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatHeightWeightAndOneOfRatiosPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(2).hratio(5)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosAndOneOfSizesPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).wmin(3).wratio(4).hratio(5)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpecifiedOnly() + throws ValidationException { + + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(2)); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpecifiedOnly() + throws ValidationException { + + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(3).wratio(4).hratio(5)); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatSizesAndRatiosPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), identity()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} (for static size " + + "requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(null).w(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(0).w(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(null)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(0)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatHeightIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(-1).w(2)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatWidthIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(2).w(-1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(null).wratio(2).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define" + + " a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(0).wratio(2).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(-1).wratio(2).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(null).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\"," + + " and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(0).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(-1).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(5).hratio(null)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(5).hratio(0)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(5).hratio(-1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenPmpDealIdIsNull() { + // given + final List givenImps = overwritePmpFirstDealInFirstImp(givenValidImps(), + dealBuilder -> dealBuilder.id(null)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenPmpDealIdIsEmptyString() { + // given + final List givenImps = overwritePmpFirstDealInFirstImp(givenValidImps(), + dealBuilder -> dealBuilder.id("")); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldThrowExceptionWhenNativeRequestEmpty() { + // given + final List givenImps = givenImps(identity()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native contains empty request value"); + } + + @Test + public void validateImpsShouldThrowExceptionWhenNativeRequestMalformed() { + // given + final List givenImps = givenImps(nativeCustomizer -> nativeCustomizer.request("broken-request")); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessageStartingWith("Error while parsing request.imp[0].native.request: JsonParseException:"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithoutErrorsForNativeSpecificContextTypes() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(500).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenContextTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(323)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is invalid. " + + "See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenContextSubTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(2).contextsubtype(100)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.contextsubtype is invalid. " + + "See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleContentValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(2).contextsubtype(11)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is 2, but contextsubtype is 11. " + + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleSocialValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(3).contextsubtype(21)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is 3, but contextsubtype is 21. " + + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleProductValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(2).contextsubtype(31)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is 2, but contextsubtype is 31. " + + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenContextSubTypeAndContextTypeValid() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenContextIsNull() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(null).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenSubTypeContextIsNull() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(null).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenEventTrackersOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(323).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.eventtrackers[0].event is invalid. See section 7.6: " + + "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenEventTrackerEmptyMethods() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(1).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.eventtrackers[0].method is required. " + + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenEventTrackerInvalidMethod() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(1).methods(singletonList(3)).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.eventtrackers[0].methods[0] is invalid. " + + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenValidEventTracker() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(1).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenEventTrackerHasSpecificType() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(500).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithoutErrorsForNativeSpecificPlacementTypes() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.plcmttype(500).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenPlacementTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.plcmttype(323)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.plcmttype is invalid. " + + "See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenAssetsContainsZeroElements() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(emptyList())); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets must be an array containing at least one object"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenElementInAssetsHasWhichIsNotUnique() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(asList( + Asset.builder().id(1).build(), + // this should get ID set on second iteration (i = 1) and result in conflict with previous id + Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[1].id is already being used by another asset. " + + "Each asset ID must be unique."); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndImage() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().build()) + .img(ImageObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0] must define at most one of" + + " {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndVideo() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().build()) + .video(VideoObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0] must define at most one of" + + " {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndData() + throws JsonProcessingException { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().build()) + .data(DataObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0] must define at most one of" + + " {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndVideo() + throws JsonProcessingException { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .img(ImageObject.builder().build()) + .video(VideoObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndData() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .img(ImageObject.builder().build()) + .data(DataObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenHasZeroTitleLen() throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().len(0).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenHasNullTitleLen() throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().len(null).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenDataTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .data(DataObject.builder().type(100).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].data.type is invalid. See section 7.4: " + + "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithoutErrorsWhenDataHasSpecicNativeTypes() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .data(DataObject.builder().type(500).build()).build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMimes() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder().mimes(emptyList()).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.mimes must be an array with at least one" + + " MIME type"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMinDuration() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(null) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasMinDurationLessThanOne() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(0) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMaxDuration() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(null) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasMaxDurationLessThanOne() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(0) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyProtocols() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(0) + .protocols(emptyList()) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoProtocolsOutOfPossibleValues() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(0) + .protocols(singletonList(20)) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenNativeVideoIsValid() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(2) + .protocols(singletonList(0)) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.protocols[0] must be in the range [1, 10]." + + " Got 0"); + } + + @Test + public void validateImpsShouldUpdateNativeRequestAssetsIds() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(asList(Asset.builder().build(), Asset.builder().build()))); + + // when + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + + assertThat(givenImps).hasSize(1) + .extracting(Imp::getXNative).doesNotContainNull() + .extracting(Native::getRequest).doesNotContainNull() + .extracting(req -> mapper.readValue(req, Request.class)) + .flatExtracting(Request::getAssets) + .flatExtracting(Asset::getId) + .containsOnly(0, 1); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() { + // given + final List givenImps = singletonList(validImpBuilder() + .metric(singletonList(Metric.builder().type(null).build())).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("Missing request.imp[0].metric[0].type"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenMetricValueIsNotValid() { + // given + final List givenImps = singletonList(validImpBuilder() + .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].metric[0].value must be in the range [0.0, 1.0]"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenNoImpExtPrebidPresent() { + // given + final List givenImps = singletonList(validImpBuilder().ext(null).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid must be defined"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidIsNotObject() { + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", "test"))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid must an object type"); + } + + @Test + public void validateImpsShouldReturnValidationMessagesWhenExtImpPrebidBidderWasNotDefined() { + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("attr", "value")))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.bidder must be defined"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidBiddersNotDefinedForStoredBidResponse() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .storedAuctionResponse(ExtStoredAuctionResponse.of("id", null, null)) + .build()); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.bidder should be defined for storedbidresponse"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseBidderMissed() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedbidresponse.bidder was not defined"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseIdMissed() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", null))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("Id was not defined for request.imp[0].ext.prebid.storedbidresponse.id"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotValidBidder() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + given(bidderCatalog.isValidName(eq("bidder"))).willReturn(false); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedbidresponse.bidder is not valid bidder"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotInImpExtPrebidBidder() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + given(bidderCatalog.isValidName(eq("bidder"))).willReturn(true); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedbidresponse.bidder does not have correspondent" + + " bidder parameters"); + } + + @Test + public void validateImpsShouldReturnEmptyMessagesWhenExtImpPrebidBidderWasMissedAndHasStoredAuctionResponseWas() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("storedauctionresponse", + mapper.createObjectNode().put("id", "1"))))).build()); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidBidderIsNotObject() { + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", "test")))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.bidder must be an object type"); + } + + @Test + public void validateImpsShouldReturnWarningAndDropBidderWhenImpExtPrebidBidderIsUnknown() + throws ValidationException { + + // given + final List givenImps = givenValidImps(); + given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages) + .containsExactly("WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder contains unknown bidder: rubicon", + "WARNING: request.imp[0].ext must contain at least one valid bidder"); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("bidder")) + .containsOnly(mapper.createObjectNode()); + } + + @Test + public void validateImpsShouldReturnWarningMessageAndDropBidderWhenBidderExtIsInvalid() throws ValidationException { + // given + final List givenImps = givenValidImps(); + given(bidderParamValidator.validate(any(), any())) + .willReturn(new LinkedHashSet<>(asList("errorMessage1", "errorMessage2"))); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages) + .containsExactly( + """ + WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: \ + request.imp[0].ext.prebid.bidder.rubicon failed validation. + errorMessage1 + errorMessage2""", + "WARNING: request.imp[0].ext must contain at least one valid bidder"); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("bidder")) + .containsOnly(mapper.createObjectNode()); + } + + @Test + public void validateImpsShouldReturnWarningMessageAndDropBidderWhenImpExtPrebidImpBidderIsUnknown() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", + Map.of("imp", singletonMap("unknownBidder", 0), "bidder", singletonMap("rubicon", 0))))) + .build()); + + given(bidderCatalog.isValidName(eq("unknownBidder"))).willReturn(false); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages).containsExactly( + "WARNING: request.imp[0].ext.prebid.imp.unknownBidder was dropped with the reason: invalid bidder"); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("imp")) + .containsOnly(mapper.createObjectNode()); + } + + @Test + public void validateImpsShouldReturnNoMessageWhenImpExtPrebidImpBidderIsAlias() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", + Map.of("imp", singletonMap("rubiconAlias", 0), "bidder", singletonMap("rubicon", 0))))) + .build()); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Map.of("rubiconAlias", "rubicon"), debugMessages); + + // then + assertThat(debugMessages).isEmpty(); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("imp")) + .containsOnly(mapper.createObjectNode().put("rubiconAlias", 0)); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenExtImpPrebidHasStoredAuctionResponseWithoutId() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( + "storedauctionresponse", mapper.createObjectNode())))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedauctionresponse.{id or seatbidobj} should be defined"); + } + + @Test + public void validateImpsShouldNotReturnValidationMessageWhenStoredAuctionResponseWithoutIdAndWithSeatBidObj() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( + "storedauctionresponse", singletonMap("seatbidobj", SeatBid.builder().build()))))) + .build()); + + // when & then + assertThatNoException().isThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)); + } + + @Test + public void validateImpsShouldReturnWarningMessageWhenExtImpPrebidHasStoredAuctionResponseSeatBidArr() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", Map.of( + "storedauctionresponse", mapper.createObjectNode() + .put("id", "1") + .set("seatbidarr", mapper.createArrayNode()))) + )).build()); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages) + .containsOnly("WARNING: request.imp[0].ext.prebid.storedauctionresponse.seatbidarr " + + "is not supported at the imp level"); + } + + // validateImp method tests + + @Test + public void validateImpShouldReturnValidationMessageWhenImpIdNull() { + // given + final Imp givenImp = validImpBuilder().id(null).build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=null] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasZeroHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenImpIdEmptyString() { + // given + final Imp givenImp = validImpBuilder().id("").build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenNoneOfMediaTypeIsPresent() { + // given + final Imp givenImp = validImpBuilder() + .video(null) + .audio(null) + .banner(null) + .xNative(null) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200] must contain at least one of \"banner\", \"video\", \"audio\", or " + + "\"native\""); + + } + + @Test + public void validateImpShouldReturnValidationMessageWhenVideoAttributeIsPresentButVideoMimesMissed() { + // given + final Imp givenImp = validImpBuilder() + .video(Video.builder().mimes(emptyList()).build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].video.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenAudioAttributePresentButAudioMimesMissed() { + // given + final Imp givenImp = validImpBuilder() + .audio(Audio.builder().mimes(emptyList()).build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].audio.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSizes() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAndValidSizes() + throws ValidationException { + + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(250) + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSizes() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldNotReturnValidationMessageForSizesIfImpIsInterstitial() throws ValidationException { + // given + final Imp givenImp = Imp.builder() + .id("11") + .instl(1) + .banner(Banner.builder() + .w(0) + .h(300) + .format(singletonList(Format.builder().w(1).h(1).build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasZeroWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasNegativeWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasNegativeHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatHWAndRatiosPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(2).wmin(3).wratio(4).hratio(5).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatHeightWeightAndOneOfRatiosPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(2).hratio(5).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosAndOneOfSizesPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).wmin(3).wratio(4).hratio(5).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpecifiedOnly() + throws ValidationException { + + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(2).build())) + .build()) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpecifiedOnly() + throws ValidationException { + + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(3).wratio(4).hratio(5).build())) + .build()) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatSizesAndRatiosPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} (for static size " + + "requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(null).w(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(0).w(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(null).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(0).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatHeightIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(-1).w(2).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatWidthIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(2).w(-1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(null).wratio(2).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define" + + " a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(0).wratio(2).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(-1).wratio(2).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(null).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\"," + + " and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(0).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(-1).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(null).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(0).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(-1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenPmpDealIdIsNull() { + // given + final Imp givenImp = validImpBuilder() + .pmp(Pmp.builder() + .deals(singletonList(Deal.builder().id(null).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenPmpDealIdIsEmptyString() { + // given + final Imp givenImp = validImpBuilder() + .pmp(Pmp.builder() + .deals(singletonList(Deal.builder().id("").build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() { + // given + final Imp givenImp = validImpBuilder() + .metric(singletonList(Metric.builder().type(null).build())).build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("Missing imp[id=200].metric[0].type"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenMetricValueIsNotValid() { + // given + final Imp givenImp = validImpBuilder() + .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].metric[0].value must be in the range [0.0, 1.0]"); + } + + private static List givenImps(UnaryOperator nativeCustomizer) { + return singletonList(validImpBuilder().xNative(nativeCustomizer.apply(Native.builder()).build()).build()); + } + + private static List givenNativeImps(UnaryOperator nativeRequestCustomizer) + throws JsonProcessingException { + + return singletonList(validImpBuilder() + .xNative(Native.builder() + .request(mapper.writeValueAsString(nativeRequestCustomizer.apply( + Request.builder()).build())) + .build()) + .build()); + } + + private static List overwriteBannerFormatInFirstImp(List imps, + UnaryOperator formatModifier) { + + final Banner banner = imps.getFirst().getBanner().toBuilder() + .format(singletonList(formatModifier.apply(Format.builder()).build())).build(); + + return singletonList(validImpBuilder().banner(banner).build()); + } + + private static List overwritePmpFirstDealInFirstImp(List imps, + UnaryOperator dealModifier) { + + final Pmp pmp = imps.getFirst().getPmp().toBuilder() + .deals(singletonList(dealModifier.apply(dealModifier.apply(Deal.builder())).build())).build(); + + return singletonList(validImpBuilder().pmp(pmp).build()); + } + + private static List givenValidImps() { + return singletonList(validImpBuilder().build()); + } + + private static Imp.ImpBuilder validImpBuilder() { + return Imp.builder().id("200") + .video(Video.builder().mimes(singletonList("vmime")) + .build()) + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(1).build())) + .build()) + .pmp(Pmp.builder().deals(singletonList(Deal.builder().id("1").build())).build()) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))); + } + +} diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index b047a9ecde0..29e12ea6b66 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -1,37 +1,20 @@ package org.prebid.server.validation; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; -import com.iab.openrtb.request.App.AppBuilder; -import com.iab.openrtb.request.Asset; -import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.DataObject; import com.iab.openrtb.request.Deal; -import com.iab.openrtb.request.Deal.DealBuilder; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.EventTracker; import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Format.FormatBuilder; -import com.iab.openrtb.request.ImageObject; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Metric; -import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Request; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Site.SiteBuilder; -import com.iab.openrtb.request.TitleObject; -import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.request.Video; -import com.iab.openrtb.request.VideoObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,7 +28,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -56,8 +38,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; @@ -66,21 +46,20 @@ import java.math.BigDecimal; import java.util.Collections; import java.util.EnumMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.function.UnaryOperator; +import java.util.List; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -90,8 +69,8 @@ public class RequestValidatorTest extends VertxTest { @Mock(strictness = LENIENT) private BidderCatalog bidderCatalog; - @Mock(strictness = LENIENT) - private BidderParamValidator bidderParamValidator; + @Mock + private ImpValidator impValidator; @Mock private Metrics metrics; @@ -99,11 +78,10 @@ public class RequestValidatorTest extends VertxTest { @BeforeEach public void setUp() { - given(bidderParamValidator.validate(any(), any())).willReturn(Collections.emptySet()); given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(true); given(bidderCatalog.isActive(eq(RUBICON))).willReturn(true); - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, false); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, false); } @Test @@ -198,150 +176,110 @@ public void validateShouldReturnValidationMessageWhenNumberOfImpsIsZero() { } @Test - public void validateShouldReturnValidationMessageWhenImpIdNull() { + public void validateShouldReturnValidationMessageWhenAliasesKeyDoesntContainAliasgvlidsKey() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().id(null).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("pubmatic", "rubicon")) + .aliasgvlids(singletonMap("between", 2)) + .build())) .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0] missing required field: \"id\""); + assertThat(result.getErrors()) + .containsExactly("request.ext.prebid.aliasgvlids. vendorId 2 refers to unknown bidder alias: between"); } @Test - public void validateShouldReturnValidationMessageWhenImpIdEmptyString() { + public void validateShouldReturnValidationMessageWhenAliasgvlidsValueLowerThatOne() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().id("").build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("pubmatic", "rubicon")) + .aliasgvlids(singletonMap("pubmatic", 0)) + .build())) .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0] missing required field: \"id\""); + assertThat(result.getErrors()) + .containsExactly("request.ext.prebid.aliasgvlids. Invalid vendorId 0 for alias: pubmatic. " + + "Choose a different vendorId, or remove this entry."); } @Test - public void validateShouldReturnValidationMessageWhenNoneOfMediaTypeIsPresent() { + public void validateShouldReturnValidationMessageWhenSiteIdAndPageIsNull() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .video(null) - .audio(null) - .banner(null) - .xNative(null) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id(null).build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or " - + "\"native\""); + .containsOnly("request.site should include at least one of request.site.id or request.site.page"); } @Test - public void validateShouldReturnValidationMessageWhenVideoAttributeIsPresentButVideaMimesMissed() { + public void validateShouldReturnValidationMessageWhenSiteIdIsEmptyStringAndPageIsNull() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .video(Video.builder().mimes(emptyList()) - .build()) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].video.mimes must contain at least one supported MIME type"); + .containsOnly("request.site should include at least one of request.site.id or request.site.page"); } @Test - public void validateShouldReturnValidationMessageWhenAudioAttributePresentButAudioMimesMissed() { + public void validateShouldReturnEmptyValidationMessagesWhenPageIdIsNullAndSiteIdIsPresent() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .audio(Audio.builder().mimes(emptyList()) - .build()) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page(null).build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].audio.mimes must contain at least one supported MIME type"); + assertThat(result.hasErrors()).isFalse(); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSizes() { + public void validateShouldEmptyValidationMessagesWhenSitePageIsEmptyString() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .format(null) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page("").build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + assertThat(result.hasErrors()).isFalse(); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAndValidSizes() { + public void validateShouldReturnValidationMessageWhenSiteIdAndPageBothEmpty() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .h(250) - .format(null) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").page("").build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.hasErrors()).isFalse(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.site should include at least one of request.site.id or request.site.page"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSizes() { + public void validateShouldReturnValidationMessageWhenSiteExtAmpIsNegative() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .site(Site.builder().id("id").page("page").ext(ExtSite.of(-1, null)).build()) .build(); // when @@ -349,23 +287,14 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSi // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHeight() { + public void validateShouldReturnValidationMessageWhenSiteExtAmpIsGreaterThanOne() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .site(Site.builder().id("id").page("page").ext(ExtSite.of(2, null)).build()) .build(); // when @@ -373,73 +302,44 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHe // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWidth() { + public void validateShouldFailWhenDoohIdAndVenuetypeAreNulls() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final Dooh invalidDooh = Dooh.builder().id(null).venuetype(null).build(); + final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroHeight() { + public void validateShouldFailWhenDoohIdIsNullAndVenuetypeIsEmpty() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .h(0) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final Dooh invalidDooh = Dooh.builder().id(null).venuetype(Collections.emptyList()).build(); + final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasZeroHeight() { + public void validateShouldReturnValidationMessageWhenRequestAppAndRequestSiteBothMissed() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .h(0) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .site(null) + .app(null) + .dooh(null) .build(); // when @@ -447,109 +347,84 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroHeight() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly("One of request.site or request.app or request.dooh must be defined"); } @Test - public void validateShouldNotReturnValidationMessageForSizesIfImpIsInterstitial() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .instl(1) - .banner(Banner.builder() - .w(0) - .h(300) - .format(singletonList(Format.builder().w(1).h(1).build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); - + public void validateShouldFailWhenDoohSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .dooh(Dooh.builder().build()) + .app(App.builder().build()) + .site(Site.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then - assertThat(result.getErrors()).isEmpty(); + verify(metrics).updateAlertsMetrics(MetricName.general); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.app and request.dooh and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenAliasesKeyDoesntContainAliasgvlidsKey() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("pubmatic", "rubicon")) - .aliasgvlids(singletonMap("between", 2)) - .build())) - .build(); - + public void validateShouldFailWhenSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .app(App.builder().build()) + .site(Site.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then - assertThat(result.getErrors()) - .containsExactly("request.ext.prebid.aliasgvlids. vendorId 2 refers to unknown bidder alias: between"); + verify(metrics).updateAlertsMetrics(MetricName.general); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.app and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenAliasgvlidsValueLowerThatOne() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("pubmatic", "rubicon")) - .aliasgvlids(singletonMap("pubmatic", 0)) - .build())) - .build(); - + public void validateShouldFailWhenDoohAndSiteArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .dooh(Dooh.builder().build()) + .site(Site.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then - assertThat(result.getErrors()) - .containsExactly("request.ext.prebid.aliasgvlids. Invalid vendorId 0 for alias: pubmatic. " - + "Choose a different vendorId, or remove this entry."); + verify(metrics).updateAlertsMetrics(MetricName.general); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.dooh and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroWidth() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(0) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); - + public void validateShouldFailWhenDoohAndAppArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .dooh(Dooh.builder().build()) + .app(App.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.app and request.dooh and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasZeroWidth() { + public void validateShouldReturnValidationMessageWhenMinWidthPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(0) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) + .build()) .build(); // when @@ -557,23 +432,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroWidth() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeWidth() { + public void validateShouldReturnValidationMessageWhenMinWidthPercIsLessThanZero() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(-300) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) + .build()) .build(); // when @@ -581,24 +449,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasNegativeWidth() { + public void validateShouldReturnValidationMessageWhenMinWidthPercGreaterThanHundred() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(-300) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) + .build()) .build(); // when @@ -606,23 +466,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeWidth() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeHeight() { + public void validateShouldReturnValidationMessageWhenMinHeightPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(-300) - .w(600) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) + .build()) .build(); // when @@ -631,23 +484,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega // then assertThat(result.getErrors()).hasSize(1) .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasNegativeHeight() { + public void validateShouldReturnValidationMessageWhenMinHeightPercIsLessThanZero() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(-300) - .w(600) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) + .build()) .build(); // when @@ -655,59 +501,68 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeHeight() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly( + "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatHWAndRatiosPresent() { + public void validateShouldReturnValidationMessageWhenMinHeightPercGreaterThanHundred() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(2).wmin(3).wratio(4).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder() + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " - + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + .containsOnly( + "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatHeightWeightAndOneOfRatiosPresent() { + public void validateShouldReturnEmptyValidationMessagesWhenBidRequestIsOk() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(2).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder().build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " - + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosAndOneOfSizesPresent() { + public void validateShouldReturnValidationMessageWhenRequestHaveDuplicatedImpIds() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).wmin(3).wratio(4).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder() + .imp(asList(Imp.builder() + .id("11") + .build(), + Imp.builder() + .id("11") + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " - + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + .containsOnly("request.imp[0].id and request.imp[1].id are both \"11\". Imp IDs must be unique."); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpecifiedOnly() { + public void validateShouldNotReturnValidationMessageIfUserExtIsEmptyJsonObject() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(2)); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder().build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -717,10 +572,11 @@ public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpec } @Test - public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpecifiedOnly() { + public void validateShouldNotReturnErrorMessageWhenRegsIsEmptyObject() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(3).wratio(4).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder() + .regs(Regs.builder().build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -730,1575 +586,32 @@ public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpe } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatSizesAndRatiosPresent() { + public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoValues() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), identity()); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(emptyMap())) + .build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} (for static size " - + "requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive"); + .containsOnly("request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined." + + " If none exist, then request.user.ext.prebid should not be defined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(null).w(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(0).w(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(null)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(0)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatHeightIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(-1).w(2)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatWidthIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(2).w(-1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(null).wratio(2).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define" - + " a valid \"wmin\", \"wratio\", and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(0).wratio(2).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define " - + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(-1).wratio(2).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define " - + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(null).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\"," - + " and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(0).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " - + "\"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(-1).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " - + "\"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(5).hratio(null)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" - + " \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(5).hratio(0)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" - + " \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(5).hratio(-1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" - + " \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenPmpDealIdIsNull() { - // given - final BidRequest bidRequest = overwritePmpFirstDealInFirstImp(validBidRequestBuilder().build(), - dealBuilder -> Deal.builder().id(null)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].pmp.deals[0] missing required field: \"id\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenPmpDealIdIsEmptyString() { - // given - final BidRequest bidRequest = overwritePmpFirstDealInFirstImp(validBidRequestBuilder().build(), - dealBuilder -> Deal.builder().id("")); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].pmp.deals[0] missing required field: \"id\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteIdAndPageIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id(null).build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site should include at least one of request.site.id or request.site.page"); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteIdIsEmptyStringAndPageIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site should include at least one of request.site.id or request.site.page"); - } - - @Test - public void validateShouldReturnEmptyValidationMessagesWhenPageIdIsNullAndSiteIdIsPresent() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page(null).build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.hasErrors()).isFalse(); - } - - @Test - public void validateShouldEmptyValidationMessagesWhenSitePageIsEmptyString() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page("").build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.hasErrors()).isFalse(); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteIdAndPageBothEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").page("").build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site should include at least one of request.site.id or request.site.page"); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteExtAmpIsNegative() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .site(Site.builder().id("id").page("page").ext(ExtSite.of(-1, null)).build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteExtAmpIsGreaterThanOne() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .site(Site.builder().id("id").page("page").ext(ExtSite.of(2, null)).build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); - } - - @Test - public void validateShouldFailWhenDoohIdAndVenuetypeAreNulls() { - // given - final Dooh invalidDooh = Dooh.builder().id(null).venuetype(null).build(); - final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); - } - - @Test - public void validateShouldFailWhenDoohIdIsNullAndVenuetypeIsEmpty() { - // given - final Dooh invalidDooh = Dooh.builder().id(null).venuetype(Collections.emptyList()).build(); - final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); - } - - @Test - public void validateShouldReturnValidationMessageWhenRequestAppAndRequestSiteBothMissed() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .site(null) - .app(null) - .dooh(null) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("One of request.site or request.app or request.dooh must be defined"); - } - - @Test - public void validateShouldFailWhenDoohSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .dooh(Dooh.builder().build()) - .app(App.builder().build()) - .site(Site.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - verify(metrics).updateAlertsMetrics(MetricName.general); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.app and request.dooh and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldFailWhenSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .app(App.builder().build()) - .site(Site.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - verify(metrics).updateAlertsMetrics(MetricName.general); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.app and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldFailWhenDoohAndSiteArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .dooh(Dooh.builder().build()) - .site(Site.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - verify(metrics).updateAlertsMetrics(MetricName.general); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.dooh and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldFailWhenDoohAndAppArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .dooh(Dooh.builder().build()) - .app(App.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.app and request.dooh and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinWidthPercIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinWidthPercIsLessThanZero() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinWidthPercGreaterThanHundred() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinHeightPercIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinHeightPercIsLessThanZero() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinHeightPercGreaterThanHundred() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnEmptyValidationMessagesWhenBidRequestIsOk() { - // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenNoImpExtPrebidPresent() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().ext(null).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid must be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenImpExtPrebidIsNotObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().ext(mapper.valueToTree(singletonMap("prebid", "test"))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid must an object type"); - } - - @Test - public void validateShouldReturnValidationMessagesWhenExtImpPrebidBidderWasNotDefined() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("attr", "value")))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.bidder must be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenImpExtPrebidBiddersNotDefinedForStoredBidResponse() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .storedAuctionResponse(ExtStoredAuctionResponse.of("id", null)) - .build()); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.bidder should be defined for storedbidresponse"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderMissed() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "id"))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder was not defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseIdMissed() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", null))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Id was not defined for request.imp[0].ext.prebid.storedbidresponse.id"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotValidBidder() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - given(bidderCatalog.isValidName(eq("bidder"))).willReturn(false); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder is not valid bidder"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotInImpExtPrebidBidder() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - given(bidderCatalog.isValidName(eq("bidder"))).willReturn(true); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder does not have correspondent" - + " bidder parameters"); - } - - @Test - public void validateShouldReturnEmptyMessagesWhenExtImpPrebidBidderWasMissedAndHasStoredAuctionResponseWas() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("storedauctionresponse", - mapper.createObjectNode().put("id", "1"))))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenExtImpPrebidHasStoredAuctionResponseWithoutId() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( - "storedauctionresponse", mapper.createObjectNode())))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()) - .containsOnly("request.imp[0].ext.prebid.storedauctionresponse.id should be defined"); - assertThat(result.getWarnings()).isEmpty(); - } - - @Test - public void validateShouldReturnWarningMessageWhenExtImpPrebidHasStoredAuctionResponseSeatBidArr() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", Map.of( - "storedauctionresponse", mapper.createObjectNode() - .put("id", "1") - .set("seatbidarr", mapper.createArrayNode()))) - )).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getWarnings()) - .containsOnly("WARNING: request.imp[0].ext.prebid.storedauctionresponse.seatbidarr " - + "is not supported at the imp level"); - } - - @Test - public void validateShouldReturnValidationMessageWhenImpExtPrebidBidderIsNotObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", "test")))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.bidder must be an object type"); - } - - @Test - public void validateShouldReturnWarningAndDropBidderWhenImpExtPrebidBidderIsUnknown() { - // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getWarnings()).hasSize(2) - .containsOnly("WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: " - + "request.imp[0].ext.prebid.bidder contains unknown bidder: rubicon", - "WARNING: request.imp[0].ext must contain at least one valid bidder"); - assertThat(bidRequest.getImp()) - .extracting(Imp::getExt) - .extracting(impExt -> impExt.get("prebid")) - .extracting(prebid -> prebid.get("bidder")) - .containsOnly(mapper.createObjectNode()); - } - - @Test - public void validateShouldReturnWarningMessageAndDropBidderWhenBidderExtIsInvalid() { - // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - given(bidderParamValidator.validate(any(), any())) - .willReturn(new LinkedHashSet<>(asList("errorMessage1", "errorMessage2"))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getWarnings()) - .containsExactly( - """ - WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: \ - request.imp[0].ext.prebid.bidder.rubicon failed validation. - errorMessage1 - errorMessage2""", - "WARNING: request.imp[0].ext must contain at least one valid bidder"); - assertThat(bidRequest.getImp()) - .extracting(Imp::getExt) - .extracting(impExt -> impExt.get("prebid")) - .extracting(prebid -> prebid.get("bidder")) - .containsOnly(mapper.createObjectNode()); - } - - @Test - public void validateShouldNotReturnValidationMessageIfUserExtIsEmptyJsonObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder().build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnErrorMessageWhenRegsIsEmptyObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .regs(Regs.builder().build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoValues() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(emptyMap())) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined." - + " If none exist, then request.user.ext.prebid should not be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsHasNullElement() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, singletonList(null))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidpermissions[i] can't be null"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList(ExtRequestPrebidDataEidPermissions.of("source", null)))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" - + " null"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList(ExtRequestPrebidDataEidPermissions.of("source", emptyList())))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" - + " null"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderIsNotRecognizedBidder() { - // given - given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(false); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: 'bidder1'"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderHasBlankValue() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList(" "))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: ' '"); - } - - @Test - public void validateShouldNotReturnValidationErrorWhenBidderIsAlias() { - // given - given(bidderCatalog.isValidName(eq("bidder1Alias"))).willReturn(false); - given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(true); - given(bidderCatalog.isActive(eq("bidder1"))).willReturn(true); - - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("bidder1Alias", "bidder1")) - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnValidationErrorWhenBidderIsAsterisk() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSource() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of(null, singletonList("bidder1"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Missing required value request.ext.prebid.data.eidPermissions[].source"); - } - - @Test - public void validateShouldReturnValidationMessageWhenCantParseTargetingPriceGranularity() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(new TextNode("pricegranularity")) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Error while parsing request.ext.prebid.targeting.pricegranularity"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesAreEmptyList() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2, emptyList()))) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: empty granularity definition supplied"); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsZero() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity - .of(2, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), - BigDecimal.valueOf(0)))))) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsMissed() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( - 2, - singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), null))))) - .build()) - .build())) - .build(); - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsNegative() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( - 2, - singletonList(ExtGranularityRange.of( - BigDecimal.valueOf(5), BigDecimal.valueOf(-1)))))) - .build()) - .build())) - .build(); - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenPrecisionIsNegative() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(-1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))))) - .build()) - .build())) - .build(); - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: precision must be non-negative"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMediaTypePriceGranularityTypesAreAllNull() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); - - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .mediatypepricegranularity(ExtMediaTypePriceGranularity.of(null, null, null)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Media type price granularity error: must have at least one media type present"); - } - - @Test - public void validateShouldReturnValidationMessageWithCorrectMediaType() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); - final ExtMediaTypePriceGranularity mediaTypePriceGranuality = ExtMediaTypePriceGranularity.of( - mapper.valueToTree(ExtPriceGranularity.of( - -1, - singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(1))))), - null, - null); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .mediatypepricegranularity(mediaTypePriceGranuality) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Banner price granularity error: precision must be non-negative"); - } - - @Test - public void validateShouldReturnValidationMessageForInvalidTargetingPrefix() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); - final String prefix = "1234567890"; - final int truncateattrchars = 10; - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .includebidderkeys(true) - .includewinners(true) - .truncateattrchars(truncateattrchars) - .prefix(prefix) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("ext.prebid.targeting: decrease prefix length or increase truncateattrchars" - + " by " + (prefix.length() + 11 - truncateattrchars) + " characters"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesContainsMissedMaxValue() { - final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(null, BigDecimal.valueOf(0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranuality)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: max value should not be missed"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValue() { - final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(BigDecimal.valueOf(2), BigDecimal.valueOf(0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranuality)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValueInTheMiddleOfRangeList() { - // given - final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(0.05)), - ExtGranularityRange.of(BigDecimal.valueOf(8), BigDecimal.valueOf(0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranuality)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsNegativeInNotLeadingElement() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(-0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsUnknownBidder() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.ext.unknown-bidder is neither a known bidder name " - + "nor an alias in request.ext.prebid.aliases"); - } - - @Test - public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidderAlias() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("unknown-bidder", "rubicon")) - .build())) - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidder() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("rubicon", "42"))) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnValidationMessageWhenEidsIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(emptyList()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidHasEmptySource() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of(null, null, null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0] missing required field: \"source\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidHasNoUids() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of("source", null, null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids must contain at least one element"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidUidsIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of("source", emptyList(), null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids must contain at least one element"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of( - "source", - singletonList(Uid.of(null, null, null)), - null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids[0] missing required field: \"id\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenAliasNameEqualsToBidderItPointsOn() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("rubicon", "rubicon")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(""" - request.ext.prebid.aliases.rubicon defines a no-op alias. \ - Choose a different alias, or remove this entry"""); - } - - @Test - public void validateShouldReturnValidationMessageWhenAliasPointOnNotValidBidderName() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("alias", "fake")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.aliases.alias refers to unknown bidder: fake"); - } - - @Test - public void validateShouldReturnValidationMessageWhenAliasPointOnDisabledBidder() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("alias", "appnexus")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - given(bidderCatalog.isValidName("appnexus")).willReturn(true); - given(bidderCatalog.isActive("appnexus")).willReturn(false); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.aliases.alias refers to disabled bidder: appnexus"); - } - - @Test - public void validateShouldReturnEmptyValidationMessagesWhenAliasesWasUsed() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("alias", "rubicon")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithErrorsWhenGdprIsNotOneOrZero() { + public void validateShouldReturnValidationMessageWhenEidsPermissionsHasNullElement() { // given final BidRequest bidRequest = validBidRequestBuilder() - .regs(Regs.builder().gdpr(2).build()) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, singletonList(null))) + .build())) .build(); // when @@ -2306,157 +619,102 @@ public void validateShouldReturnValidationResultWithErrorsWhenGdprIsNotOneOrZero // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.regs.ext.gdpr must be either 0 or 1"); - } - - @Test - public void validateShouldThrowExceptionWhenNativeRequestEmpty() { - // given - final BidRequest bidRequest = givenBidRequest(identity()); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native contains empty request value"); - } - - @Test - public void validateShouldThrowExceptionWhenNativeRequestMalformed() { - // given - final BidRequest bidRequest = givenBidRequest(nativeCustomizer -> nativeCustomizer.request("broken-request")); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error) - .startsWith("Error while parsing request.imp[0].native.request: JsonParseException:"); - }); - } - - @Test - public void validateShouldReturnValidationResultWithoutErrorsForNativeSpecificContextTypes() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(500).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithErrorWhenContextTypeOutOfPossibleValuesRange() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(323)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is invalid. " - + "See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidpermissions[i] can't be null"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenContextSubTypeOutOfPossibleValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsNull() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(2).contextsubtype(100)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList(ExtRequestPrebidDataEidPermissions.of("source", null)))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.contextsubtype is invalid. " - + "See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" + + " null"); } @Test - public void validateShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleContentValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsEmpty() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(2).contextsubtype(11)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList(ExtRequestPrebidDataEidPermissions.of("source", emptyList())))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is 2, but contextsubtype is 11. " - + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" + + " null"); } @Test - public void validateShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleSocialValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderIsNotRecognizedBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(3).contextsubtype(21)); + given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(false); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is 3, but contextsubtype is 21. " - + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly( + "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: 'bidder1'"); } @Test - public void validateShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleProductValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderHasBlankValue() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(2).contextsubtype(31)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList(" "))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is 2, but contextsubtype is 31. " - + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: ' '"); } @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenContextSubTypeAndContextTypeValid() - throws JsonProcessingException { + public void validateShouldNotReturnValidationErrorWhenBidderIsAlias() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } + given(bidderCatalog.isValidName(eq("bidder1Alias"))).willReturn(false); + given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(true); + given(bidderCatalog.isActive(eq("bidder1"))).willReturn(true); - @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenContextIsNull() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(null).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("bidder1Alias", "bidder1")) + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -2466,11 +724,15 @@ public void validateShouldReturnValidationResultWithEmptyErrorWhenContextIsNull( } @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenSubTypeContextIsNull() - throws JsonProcessingException { + public void validateShouldNotReturnValidationErrorWhenBidderIsAsterisk() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(null).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -2480,522 +742,473 @@ public void validateShouldReturnValidationResultWithEmptyErrorWhenSubTypeContext } @Test - public void validateShouldReturnValidationResultWithErrorWhenEventTrackersOutOfPossibleValuesRange() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(323).build())).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.eventtrackers[0].event is invalid. See section 7.6: " - + "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); - } - - @Test - public void validateShouldReturnValidationResultWithErrorWhenEventTrackerEmptyMethods() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(1).build())).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.eventtrackers[0].method is required. " - + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); - } - - @Test - public void validateShouldReturnValidationResultWithErrorWhenEventTrackerInvalidMethod() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSource() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(1).methods(singletonList(3)).build())).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of(null, singletonList("bidder1"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.eventtrackers[0].methods[0] is invalid. " - + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); - } - - @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenValidEventTracker() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(1).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenEventTrackerHasSpecificType() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(500).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithoutErrorsForNativeSpecificPlacementTypes() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.plcmttype(500).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); + .containsOnly("Missing required value request.ext.prebid.data.eidPermissions[].source"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenPlacementTypeOutOfPossibleValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenCantParseTargetingPriceGranularity() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.plcmttype(323)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(new TextNode("pricegranularity")) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.plcmttype is invalid. " - + "See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + .containsOnly("Error while parsing request.ext.prebid.targeting.pricegranularity"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenAssetsContainsZeroElements() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenRangesAreEmptyList() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(emptyList())); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2, emptyList()))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets must be an array containing at least one object"); + .containsOnly("Price granularity error: empty granularity definition supplied"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenElementInAssetsHasWhichIsNotUnique() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsZero() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(asList( - Asset.builder().id(1).build(), - // this should get ID set on second iteration (i = 1) and result in conflict with previous id - Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity + .of(2, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), + BigDecimal.valueOf(0)))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[1].id is already being used by another asset. " - + "Each asset ID must be unique."); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndImage() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsMissed() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().build()) - .img(ImageObject.builder().build()) - .build()))); - + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( + 2, + singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), null))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0] must define at most one of" - + " {title, img, video, data}"); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndVideo() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsNegative() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().build()) - .video(VideoObject.builder().build()) - .build()))); - + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( + 2, + singletonList(ExtGranularityRange.of( + BigDecimal.valueOf(5), BigDecimal.valueOf(-1)))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0] must define at most one of" - + " {title, img, video, data}"); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndData() - throws JsonProcessingException { - + public void validateShouldReturnValidationMessageWhenPrecisionIsNegative() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().build()) - .data(DataObject.builder().build()) - .build()))); - + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(-1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0] must define at most one of" - + " {title, img, video, data}"); + .containsOnly("Price granularity error: precision must be non-negative"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndVideo() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenMediaTypePriceGranularityTypesAreAllNull() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .img(ImageObject.builder().build()) - .video(VideoObject.builder().build()) - .build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .mediatypepricegranularity(ExtMediaTypePriceGranularity.of(null, null, null)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + .containsOnly("Media type price granularity error: must have at least one media type present"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndData() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWithCorrectMediaType() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .img(ImageObject.builder().build()) - .data(DataObject.builder().build()) - .build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + final ExtMediaTypePriceGranularity mediaTypePriceGranuality = ExtMediaTypePriceGranularity.of( + mapper.valueToTree(ExtPriceGranularity.of( + -1, + singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(1))))), + null, + null); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .mediatypepricegranularity(mediaTypePriceGranuality) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + .containsOnly("Banner price granularity error: precision must be non-negative"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenHasZeroTitleLen() throws JsonProcessingException { + public void validateShouldReturnValidationMessageForInvalidTargetingPrefix() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().len(0).build()).build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + final String prefix = "1234567890"; + final int truncateattrchars = 10; + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .includebidderkeys(true) + .includewinners(true) + .truncateattrchars(truncateattrchars) + .prefix(prefix) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + .containsOnly("ext.prebid.targeting: decrease prefix length or increase truncateattrchars" + + " by " + (prefix.length() + 11 - truncateattrchars) + " characters"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenHasNullTitleLen() throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().len(null).build()).build()))); + public void validateShouldReturnValidationMessageWhenRangesContainsMissedMaxValue() { + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(null, BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + .containsOnly("Price granularity error: max value should not be missed"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenDataTypeOutOfPossibleValuesRange() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .data(DataObject.builder().type(100).build()).build()))); + public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValue() { + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(BigDecimal.valueOf(2), BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0].data.type is invalid. See section 7.4: " - + "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); } @Test - public void validateShouldReturnValidationResultWithoutErrorsWhenDataHasSpecicNativeTypes() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValueInTheMiddleOfRangeList() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .data(DataObject.builder().type(500).build()).build()))); + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(0.05)), + ExtGranularityRange.of(BigDecimal.valueOf(8), BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMimes() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsNegativeInNotLeadingElement() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder().mimes(emptyList()).build()).build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(-0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0].video.mimes must be an array with at least one" - + " MIME type"); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMinDuration() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsUnknownBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(null) + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) .build()) - .build()))); + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + .containsOnly("request.user.ext.unknown-bidder is neither a known bidder name " + + "nor an alias in request.ext.prebid.aliases"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasMinDurationLessThanOne() - throws JsonProcessingException { + public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidderAlias() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(0) + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("unknown-bidder", "rubicon")) + .build())) + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) .build()) - .build()))); + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMaxDuration() - throws JsonProcessingException { + public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(null) + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(singletonMap("rubicon", "42"))) .build()) - .build()))); + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasMaxDurationLessThanOne() - throws JsonProcessingException { + public void validateShouldNotReturnValidationMessageWhenEidsIsEmpty() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(0) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(emptyList()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyProtocols() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidHasEmptySource() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(0) - .protocols(emptyList()) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(singletonList(Eid.builder().build())) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + .containsOnly("request.user.eids[0] missing required field: \"source\""); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoProtocolsOutOfPossibleValues() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenAliasNameEqualsToBidderItPointsOn() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(0) - .protocols(singletonList(20)) - .build()) - .build()))); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("rubicon", "rubicon")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + .containsOnly(""" + request.ext.prebid.aliases.rubicon defines a no-op alias. \ + Choose a different alias, or remove this entry"""); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenNativeVideoIsValid() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenAliasPointOnNotValidBidderName() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(2) - .protocols(singletonList(0)) - .build()) - .build()))); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "fake")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0].video.protocols[0] must be in the range [1, 10]." - + " Got 0"); + .containsOnly("request.ext.prebid.aliases.alias refers to unknown bidder: fake"); } @Test - public void validateShouldUpdateNativeRequestAssetsIds() throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenAliasPointOnDisabledBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(asList(Asset.builder().build(), Asset.builder().build()))); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "appnexus")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); + given(bidderCatalog.isValidName("appnexus")).willReturn(true); + given(bidderCatalog.isActive("appnexus")).willReturn(false); // when - target.validate(bidRequest, null); + final ValidationResult result = target.validate(bidRequest, null); - assertThat(bidRequest.getImp()).hasSize(1) - .extracting(Imp::getXNative).doesNotContainNull() - .extracting(Native::getRequest).doesNotContainNull() - .extracting(req -> mapper.readValue(req, Request.class)) - .flatExtracting(Request::getAssets) - .flatExtracting(Asset::getId) - .containsOnly(0, 1); + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.aliases.alias refers to disabled bidder: appnexus"); } @Test - public void validateShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() { + public void validateShouldReturnEmptyValidationMessagesWhenAliasesWasUsed() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .metric(singletonList(Metric.builder().type(null).build())).build())) - .build(); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "rubicon")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .element(0).isEqualTo("Missing request.imp[0].metric[0].type"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationMessageWhenMetricValueIsNotValid() { + public void validateShouldReturnValidationResultWithErrorsWhenGdprIsNotOneOrZero() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build())) + .regs(Regs.builder().gdpr(2).build()) .build(); // when @@ -3003,7 +1216,7 @@ public void validateShouldReturnValidationMessageWhenMetricValueIsNotValid() { // then assertThat(result.getErrors()).hasSize(1) - .element(0).isEqualTo("request.imp[0].metric[0].value must be in the range [0.0, 1.0]"); + .containsOnly("request.regs.ext.gdpr must be either 0 or 1"); } @Test @@ -3182,43 +1395,33 @@ public void validateShouldReturnValidationMessageWhenMultipleSchainsForSameBidde } @Test - public void validateShouldReturnValidationMessageWhenRequestHaveDuplicatedImpIds() { + public void validateShouldReturnValidationMessageWhenImpValidationFailed() throws ValidationException { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(asList(Imp.builder() - .id("11") - .build(), - Imp.builder() - .id("11") - .build())) - .build(); + doThrow(new ValidationException("imp[0] validation failed")) + .when(impValidator).validateImps(any(), any(), any()); + + final BidRequest bidRequest = validBidRequestBuilder().build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].id and request.imp[1].id are both \"11\". Imp IDs must be unique."); + assertThat(result.getErrors()).containsOnly("imp[0] validation failed"); } - private static BidRequest givenBidRequest( - UnaryOperator nativeCustomizer) { - return validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .xNative(nativeCustomizer.apply(Native.builder()).build()).build())).build(); - } + @Test + public void validateShouldReturnWarningMessageWhenImpValidationWarns() throws ValidationException { + // given + doAnswer(invocation -> ((List) invocation.getArgument(2)).add("imp[0] validation warning")) + .when(impValidator).validateImps(any(), any(), any()); - private static BidRequest givenBidRequestWithNativeRequest( - UnaryOperator nativeRequestCustomizer) - throws JsonProcessingException { - return validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .xNative(Native.builder() - .request(mapper.writeValueAsString(nativeRequestCustomizer.apply( - Request.builder()).build())) - .build()) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().build(); + + // when + final ValidationResult result = target.validate(bidRequest, null); + + // then + assertThat(result.getWarnings()).containsOnly("imp[0] validation warning"); } private static BidRequest.BidRequestBuilder validBidRequestBuilder() { @@ -3239,28 +1442,4 @@ private static Imp.ImpBuilder validImpBuilder() { .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))); } - private static BidRequest overwriteBannerFormatInFirstImp( - BidRequest bidRequest, UnaryOperator formatModifier) { - final Banner banner = bidRequest.getImp().getFirst().getBanner().toBuilder() - .format(singletonList(formatModifier.apply(Format.builder()).build())).build(); - - return bidRequest.toBuilder().imp(singletonList(validImpBuilder().banner(banner).build())).build(); - } - - private static BidRequest overwritePmpFirstDealInFirstImp( - BidRequest bidRequest, UnaryOperator dealModifier) { - final Pmp pmp = bidRequest.getImp().getFirst().getPmp().toBuilder() - .deals(singletonList(dealModifier.apply(dealModifier.apply(Deal.builder())).build())).build(); - - return bidRequest.toBuilder().imp(singletonList(validImpBuilder().pmp(pmp).build())).build(); - } - - private static BidRequest.BidRequestBuilder requestWithBothSiteAndApp( - BidRequest.BidRequestBuilder builder, - UnaryOperator siteModifier, - UnaryOperator appModifier) { - - return builder.site(siteModifier.apply(Site.builder()).build()) - .app(appModifier.apply(App.builder()).build()); - } } diff --git a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java index ead616d13b7..ffb6c9e6804 100644 --- a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java @@ -14,6 +14,8 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; @@ -24,6 +26,7 @@ import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -34,6 +37,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.prebid.server.settings.model.BidValidationEnforcement.enforce; import static org.prebid.server.settings.model.BidValidationEnforcement.skip; import static org.prebid.server.settings.model.BidValidationEnforcement.warn; @@ -47,6 +51,9 @@ public class ResponseBidValidatorTest extends VertxTest { @Mock private Metrics metrics; + @Mock + private BidRejectionTracker bidRejectionTracker; + private ResponseBidValidator target; @Mock(strictness = LENIENT) @@ -70,6 +77,7 @@ public void validateShouldFailedIfBidderBidCurrencyIsIncorrect() { // then assertThat(result.getErrors()).containsOnly("BidResponse currency \"invalid\" is not valid"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -80,6 +88,7 @@ public void validateShouldFailIfMissingBid() { // then assertThat(result.getErrors()).containsOnly("Empty bid object submitted"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -90,6 +99,7 @@ public void validateShouldFailIfBidHasNoId() { // then assertThat(result.getErrors()).containsOnly("Bid missing required field 'id'"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -100,6 +110,7 @@ public void validateShouldFailIfBidHasNoImpId() { // then assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing required field 'impid'"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -113,6 +124,7 @@ public void validateShouldSuccessForDealZeroPriceBid() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -123,13 +135,14 @@ public void validateShouldFailIfBidHasNoCrid() { // then assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing creative ID"); + verifyNoInteractions(bidRejectionTracker); } @Test public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { // when - final ValidationResult result = target.validate( - givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final BidderBid givenBid = givenBid(builder -> builder.w(null).h(null)); + final ValidationResult result = target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases); // then assertThat(result.getErrors()) @@ -137,13 +150,15 @@ public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { BidResponse validation `enforce`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='nullxnull'"""); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { // when - final ValidationResult result = target.validate( - givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final BidderBid givenBid = givenBid(builder -> builder.w(150).h(150)); + final ValidationResult result = target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases); // then assertThat(result.getErrors()) @@ -151,16 +166,15 @@ public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { BidResponse validation `enforce`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='150x150'"""); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { // when - final ValidationResult result = target.validate( - givenBid(builder -> builder.w(50).h(250)), - BIDDER_NAME, - givenAuctionContext(), - bidderAliases); + final BidderBid givenBid = givenBid(builder -> builder.w(50).h(250)); + final ValidationResult result = target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases); // then assertThat(result.getErrors()) @@ -168,6 +182,8 @@ public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { BidResponse validation `enforce`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='50x250'"""); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -181,6 +197,7 @@ public void validateShouldReturnSuccessIfNonBannerBidHasAnySize() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -198,6 +215,7 @@ public void validateShouldTolerateMissingImpExtBidderNode() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -214,6 +232,7 @@ public void validateShouldReturnSuccessIfBannerBidHasInvalidSizeButAccountDoesNo // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -228,13 +247,15 @@ public void validateShouldFailIfBidHasNoCorrespondingImp() { // then assertThat(result.getErrors()) .containsOnly("Bid \"bidId1\" has no corresponding imp in request"); + verifyNoInteractions(bidRejectionTracker); } @Test public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() { // when + final BidderBid givenBid = givenBid(builder -> builder.adm("http://site.com/creative.jpg")); final ValidationResult result = target.validate( - givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + givenBid, BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), bidderAliases); @@ -245,13 +266,16 @@ public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() BidResponse validation `enforce`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=http://site.com/creative.jpg"""); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureContext() { // when + final BidderBid givenBid = givenBid(builder -> builder.adm("http%3A//site.com/creative.jpg")); final ValidationResult result = target.validate( - givenBid(builder -> builder.adm("http%3A//site.com/creative.jpg")), + givenBid, BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), bidderAliases); @@ -262,13 +286,16 @@ public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureCon BidResponse validation `enforce`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=http%3A//site.com/creative.jpg"""); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() { // when + final BidderBid givenBid = givenBid(builder -> builder.adm("//site.com/creative.jpg")); final ValidationResult result = target.validate( - givenBid(builder -> builder.adm("//site.com/creative.jpg")), + givenBid, BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), bidderAliases); @@ -279,6 +306,8 @@ public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() BidResponse validation `enforce`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=//site.com/creative.jpg"""); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -292,6 +321,7 @@ public void validateShouldReturnSuccessIfBidHasInsecureCreativeInInsecureContext // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -307,6 +337,7 @@ public void validateShouldFailedIfVideoBidHasNoNurlAndAdm() { assertThat(result.getErrors()) .containsOnly("Bid \"bidId1\" with video type missing adm and nurl"); verify(metrics).updateAdapterRequestErrorMetric(BIDDER_NAME, MetricName.badserverresponse); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -320,6 +351,7 @@ public void validateShouldReturnSuccessfulResultForValidVideoBidWithNurl() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -333,6 +365,7 @@ public void validateShouldReturnSuccessfulResultForValidVideoBidWithAdm() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -346,6 +379,7 @@ public void validateShouldReturnSuccessfulResultForValidBid() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -362,6 +396,7 @@ public void validateShouldReturnSuccessIfBannerSizeValidationNotEnabled() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -370,8 +405,9 @@ public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn( target = new ResponseBidValidator(warn, enforce, metrics, 0.01); // when + final BidderBid givenBid = givenBid(builder -> builder.w(null).h(null)); final ValidationResult result = target.validate( - givenBid(builder -> builder.w(null).h(null)), + givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases); @@ -383,6 +419,7 @@ public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn( BidResponse validation `warn`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='nullxnull'"""); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -399,6 +436,7 @@ public void validateShouldReturnSuccessIfSecureMarkupValidationNotEnabled() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -407,8 +445,9 @@ public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWar target = new ResponseBidValidator(enforce, warn, metrics, 0.01); // when + final BidderBid givenBid = givenBid(builder -> builder.adm("http://site.com/creative.jpg")); final ValidationResult result = target.validate( - givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + givenBid, BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), bidderAliases); @@ -420,19 +459,19 @@ public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWar BidResponse validation `warn`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=http://site.com/creative.jpg"""); + verifyNoInteractions(bidRejectionTracker); } @Test public void validateShouldIncrementSizeValidationErrMetrics() { // when - target.validate( - givenBid(builder -> builder.w(150).h(200)), - BIDDER_NAME, - givenAuctionContext(), - bidderAliases); + final BidderBid givenBid = givenBid(builder -> builder.w(150).h(200)); + target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases); // then verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -441,27 +480,28 @@ public void validateShouldIncrementSizeValidationWarnMetrics() { target = new ResponseBidValidator(warn, warn, metrics, 0.01); // when - target.validate( - givenBid(builder -> builder.w(150).h(200)), - BIDDER_NAME, - givenAuctionContext(), - bidderAliases); + final BidderBid givenBid = givenBid(builder -> builder.w(150).h(200)); + target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases); // then verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + verifyNoInteractions(bidRejectionTracker); } @Test public void validateShouldIncrementSecureValidationErrMetrics() { // when + final BidderBid givenBid = givenBid(builder -> builder.adm("http://site.com/creative.jpg")); target.validate( - givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + givenBid, BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), bidderAliases); // then verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + verify(bidRejectionTracker) + .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -470,14 +510,16 @@ public void validateShouldIncrementSecureValidationWarnMetrics() { target = new ResponseBidValidator(warn, warn, metrics, 0.01); // when + final BidderBid givenBid = givenBid(builder -> builder.adm("http://site.com/creative.jpg")); target.validate( - givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + givenBid, BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), bidderAliases); // then verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + verifyNoInteractions(bidRejectionTracker); } private BidRequest givenRequest(UnaryOperator impCustomizer) { @@ -522,22 +564,23 @@ private static BidderBid givenBid(BidType type, String bidCurrency, UnaryOperato return BidderBid.of(bidCustomizer.apply(bidBuilder).build(), type, bidCurrency); } - private static AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { + private AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { return AuctionContext.builder() + .bidRejectionTrackers(Map.of("bidder", bidRejectionTracker)) .account(account) .bidRequest(bidRequest) .build(); } - private static AuctionContext givenAuctionContext(BidRequest bidRequest) { + private AuctionContext givenAuctionContext(BidRequest bidRequest) { return givenAuctionContext(bidRequest, givenAccount()); } - private static AuctionContext givenAuctionContext(Account account) { + private AuctionContext givenAuctionContext(Account account) { return givenAuctionContext(givenBidRequest(identity()), account); } - private static AuctionContext givenAuctionContext() { + private AuctionContext givenAuctionContext() { return givenAuctionContext(givenBidRequest(identity()), givenAccount()); } diff --git a/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java b/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java index e182255df87..15c76f3b6c7 100644 --- a/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java +++ b/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java @@ -14,8 +14,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import java.time.Clock; diff --git a/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java b/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java index fe6bc6c337b..abc2d8af479 100644 --- a/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java +++ b/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java @@ -14,8 +14,8 @@ import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import java.time.Clock; diff --git a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json index fcac32fb76b..92abd79998c 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json +++ b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json @@ -12,6 +12,9 @@ "hb_cache_id": "fea00992-651c-44c8-b16a-b9af99fdf2dd", "hb_bidder_generic": "generic", "hb_size_genericAlias": "300x250", + "hb_env": "amp", + "hb_env_generic": "amp", + "hb_env_genericAlias": "amp", "hb_cache_host": "{{ cache.host }}", "hb_cache_path": "{{ cache.path }}", "hb_cache_host_generic": "{{ cache.host }}", diff --git a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json index 4908b67e9c1..fe8eba5c934 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json @@ -27,7 +27,8 @@ "origbidcpm": 12.09 } }, - "aid":"tid" + "aid":"tid", + "ttlseconds": 300 }, { "type": "json", @@ -60,7 +61,8 @@ "origbidcur": "USD" } }, - "aid":"tid" + "aid":"tid", + "ttlseconds": 300 } ] } diff --git a/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json index 5cc33c6206c..4d45a82bbc0 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json @@ -51,6 +51,9 @@ "ext": { "ConsentedProvidersSettings": { "consented_providers": "someConsent" + }, + "consented_providers_settings": { + "consented_providers": "someConsent" } } }, diff --git a/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json index 1d28937fef6..65febdb9a16 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json @@ -49,6 +49,9 @@ "ext": { "ConsentedProvidersSettings": { "consented_providers": "someConsent" + }, + "consented_providers_settings": { + "consented_providers": "someConsent" } } }, diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json index 9d49c702f5e..e6127bea4f0 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json @@ -11,6 +11,7 @@ "crid": "crid2", "w": 120, "h": 600, + "exp": 300, "ext": { "prebid": { "type": "banner", @@ -35,6 +36,7 @@ { "id": "31124", "impid": "impId-video-cache-update", + "exp": 1500, "price": 3, "adm": "adm1", "crid": "crid1", diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json index 5637a0f0026..0daecc354d4 100644 --- a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json @@ -27,7 +27,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json index eacb079d5fe..cc893e56246 100644 --- a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json @@ -7,7 +7,7 @@ "id": "880290288", "impid": "impId1", "price": 8.43, - "adm": "", + "adm": "", "crid": "crid1", "w": 300, "h": 250, diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json index 89d2ac57bcb..14d752bc96d 100644 --- a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json @@ -28,7 +28,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json b/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json index f086c053112..b7a0ac4311d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json index c223e8f56d3..f6f8aa08087 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json @@ -6,6 +6,7 @@ { "id": "randomid", "impid": "test-imp-id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json index f80400fe5d1..b9b61a696c0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json index ba9ea7db56d..a0bab7d6cc2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json index ee0b96cb442..0c48f3a8431 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json index 320108794b5..f4417095fa3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 11.393, "adomain": [ ], @@ -22,6 +23,7 @@ { "id": "bid_id_banner", "impid": "imp_id_banner", + "exp": 300, "price": 11.393, "adomain": [], "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json index 05c116d4aa2..25ebe533dcc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json @@ -6,6 +6,7 @@ { "id": "id", "impid": "id", + "exp": 300, "price": 46.6, "adm": "", "crid": "Dummy_supership.jp", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json index 9895195c325..f5fd5214de2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 2.184, "adm": "
\"\"
", "crid": "demo-424", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json index 92b00eb8c2b..a6a8e2bd0c8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 2.25, "adm": "", "adid": "2002", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json index 9f868cc7baa..53df688de45 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json @@ -24,4 +24,4 @@ } ], "bidid": "bid_id" -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json index f1d38a7780f..9563c1ff672 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "adm021", "adid": "19005", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json index 8d448c61116..8884760961e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json @@ -6,6 +6,7 @@ { "id": "bid_id1", "impid": "imp_id1", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json index 6ad0d2f637f..0277bb7f78d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json index 5561b33da3b..aceadcc04ac 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json index 4352d750af3..75f33a522f6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json @@ -16,6 +16,7 @@ }, "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01 } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json index beffca0d359..61bac864a49 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json @@ -6,6 +6,7 @@ { "id": "some_ad_id", "impid": "imp_id", + "exp": 300, "price": 42420.00, "adm": "some_html", "adid": "some_ad_id", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json index 76c4005d49c..3f62c1fb7db 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json @@ -6,6 +6,7 @@ { "id": "adoceanmyaozpniqismex", "impid": "imp_id", + "exp": 300, "price": 10, "adm": " ", "crid": "0af345b42983cc4bc0", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json index ba80a545eed..4edc56ade74 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json @@ -26,4 +26,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json index db0b1aeaa49..7822b00cdbb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json index dcff8f22c64..b2c52bb7d9e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.16346, "adm": "some-test-ad", "crid": "crid001", @@ -36,4 +37,4 @@ "auctiontimestamp": 1626182712962 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json index 7f09ff7886b..1682aad2c48 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json index e24dfce9f48..3c4fc34a311 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 6.66, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json index 5b7e94562fd..073a812bcdb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json index 0d05040052c..1d301f75624 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json @@ -6,6 +6,7 @@ { "id": "22e26bd9a702bc1", "impid": "22e26bd9a702bc", + "exp": 300, "price": 1.090, "adm": "Tag_Example", "adomain": [ diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json index d480aae971a..e1341dbdcca 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adid": "adid001", "cid": "cid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json index b9d85d4e632..855d418643f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json index 03c5ee91218..d5b04833e91 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json @@ -17,4 +17,4 @@ "group": 0 } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json index 23465125a4e..809b063e228 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json rename to src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json similarity index 99% rename from src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json rename to src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json index 15d06b7c923..1da8f18279d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json @@ -17,4 +17,4 @@ "group": 0 } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json index 458b300cb66..b73512ad65c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json new file mode 100644 index 00000000000..ab7be17fc96 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "supplierId": "testPublisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json new file mode 100644 index 00000000000..c9191a06125 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json @@ -0,0 +1,16 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json new file mode 100644 index 00000000000..8077266f37e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adtonos": { + "supplierId": "testPublisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json new file mode 100644 index 00000000000..c5bfdb6d592 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adtonos", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adtonos": "{{ adtonos.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json index 60c2ad42bab..d786080f717 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "h": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json index df8ec148aa1..92ba70c5c42 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json index eccc7f38dec..a6c118e0913 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json index 81b8aa40e9e..0961b1f67ec 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json index e291739474c..a4c0edc3e09 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json index ec08af30179..96aa18de5d8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json index 1dff571757c..f5ee4e08e9c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "mtype": 1, "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json index d2f3908c4b3..413a3ffe241 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json index 5b8ce2dfb32..7010f75f5e6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 10, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json index e291739474c..a4c0edc3e09 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json index f3b649ebbec..b61aebbccd6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json index ca0b59e06b3..b8ccdb2d3b3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json index dc3186c5778..ec393ab7ca0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json index e9c1f602280..2122bfc623a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json b/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json index da99c54e188..15ae12d04c5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json @@ -4,19 +4,22 @@ "type": "xml", "value": "some-test-ad-3", "aid": "bid_id", - "key": "2.0_IAB10-1_0s_{{uuid}}" + "key": "2.0_IAB10-1_0s_{{uuid}}", + "ttlseconds": 1500 }, { "type": "xml", "value": "some-test-ad", "aid": "bid_id", - "key": "5.5_IAB20-3_0s_{{uuid}}" + "key": "5.5_IAB20-3_0s_{{uuid}}", + "ttlseconds": 1500 }, { "type": "xml", "value": "some-test-ad-2", "aid": "bid_id", - "key": "2.5_IAB18-5_0s_{{uuid}}" + "key": "2.5_IAB18-5_0s_{{uuid}}", + "ttlseconds": 1500 } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json b/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json index 4a71755b12d..f673f770ad2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json index cef76b6cec9..6b9a19e7187 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6_4.7", "nurl": "nurl_4.7", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json b/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json index 376d88dbf44..e1d18a2ecb5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 9.0, "adm": "{\"bid_id\":\"10\"}", "adid": "10", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json index 64383cd93de..c4c971e466e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json index cc260a44b94..6fca036d997 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 0.5, "adm": "some-test-ad", "adid": "29681110", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json index 37c3691752c..676eb7d802a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json @@ -6,6 +6,7 @@ { "id": "bid_id1", "impid": "imp_id1", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json index 31a15adc1f9..e0e02fc7381 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json index 1ca1cb7607c..c591ef97cfd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6_4.7", "nurl": "nurl_4.7", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json index 5338cc8c4d4..5e10a21b5de 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json @@ -6,6 +6,7 @@ { "id": "imp_idBanner", "impid": "imp_id", + "exp": 300, "price": 2.942807912826538, "adm": "
", "crid": "crid_3", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json index 9419a85783d..bed5b0e3939 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "bid_id", + "exp": 300, "price": 2.942808, "adid": "94395500", "crid": "94395500", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json index 4b0e7de3f44..1d2bac9f898 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6", "crid": "crid6", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json b/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json index ed208dd5654..cf2a6e0d031 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json index 605deba4cb1..e63089babb2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json index 80cfe99af9e..fdba12487c7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6_4.7", "nurl": "nurl_4.7", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json index eb4e503494f..0ea56280b80 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json new file mode 100644 index 00000000000..b58484a029d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidmatic": { + "source": 1000, + "siteId": 1234, + "bidFloor": 100, + "placementId": 10 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json new file mode 100644 index 00000000000..ba0b73cfaf1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 8.43, + "adm": "adm14", + "crid": "crid14", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 8.43 + } + } + ], + "seat": "bidmatic", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "bidmatic": "{{ bidmatic.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json new file mode 100644 index 00000000000..a151c6c7cbd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "bidfloor": 100, + "ext": { + "bidmatic": { + "source": 1000, + "placementId": 10, + "siteId": 1234, + "bidFloor": 100 + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json new file mode 100644 index 00000000000..3dc3f71e392 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 8.43, + "adm": "adm14", + "crid": "crid14", + "w": 300, + "h": 250 + } + ], + "seat": "bidmatic", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json index ba657da0438..399a568d088 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json index 8cb8a61c009..8c8b0128e98 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json index 523bdbbcda4..9428b19fd0a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adid": "adid001", "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json index 287c05c61aa..013cd170712 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 3.33, "adm": "adm001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json similarity index 75% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json index bfbeccf737f..8ee8e6865d7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json @@ -8,10 +8,9 @@ "h": 250 }, "ext": { - "bizzclick": { - "host": "host", + "blasto": { "accountId": "accountId", - "placementId": "placementId" + "sourceId": "sourceId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json similarity index 87% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json index d024a8f093b..22a67229971 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", @@ -22,14 +23,14 @@ } } ], - "seat": "bizzclick", + "seat": "blasto", "group": 0 } ], "cur": "USD", "ext": { "responsetimemillis": { - "bizzclick": "{{ bizzclick.response_time_ms }}" + "blasto": "{{ blasto.response_time_ms }}" }, "prebid": { "auctiontimestamp": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-request.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-request.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-response.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json index de26e4363ea..f4ddf9222af 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json index 939897d89a1..71412aa8e6c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json index 1d36233fd2d..cbbae431606 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json b/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json index e9242e76699..4d5d8f5ce9e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json b/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json index 39819c16e18..43de8398d78 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json index 6029a55596f..6107fe586a3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "mtype": 1, "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json index c5b97cc0914..f2f43ba2c1d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json @@ -6,6 +6,7 @@ { "id": "imp_id", "impid": "imp_id", + "exp": 300, "price": 2.942808, "adm": "
", "adid": "94395500", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json index 28eb0bc9e9d..7fcafb472c7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json index 4837ee79517..06fadbf0b09 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json b/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json index cbad770f0e5..9f59c942b28 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 6.66, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json index 3491c77189e..f551e12bbe4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json index c4d521102b5..d7914b5fb58 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json b/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json index 0da511e197e..20f86d6a348 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json b/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json index aadc8da3482..666a177b9c1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 3.33, "adm": "adm001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json index a5abd2ecf40..00efb79c5a1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json @@ -10,8 +10,8 @@ "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", "ext": { "connectad": { - "networkId": 12, - "siteId": 15, + "networkId": "12", + "siteId": "15", "bidfloor": 14.7 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json index 9aa7d077c16..d24913feeef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adm": "hi", "cid": "test_cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json index 7058b025b51..c511c5b8e65 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json @@ -15,8 +15,8 @@ "ext": { "tid": "${json-unit.any-string}", "bidder": { - "networkId": 12, - "siteId": 15, + "networkId": "12", + "siteId": "15", "bidfloor": 14.7 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json index 1290f6775fa..a28d27cf14a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json @@ -7,6 +7,7 @@ { "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", + "exp": 300, "price": 0.500000, "adm": "some-test-ad", "crid": "crid_10", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json index 464f7df6b6a..3ac3ccb1672 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json new file mode 100644 index 00000000000..97375afbc45 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "copper6ssp": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json new file mode 100644 index 00000000000..52b36682c8a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "copper6ssp", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "copper6ssp": "{{ copper6ssp.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json index 9444279b4a6..6a685a06ba4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json index fb1c7803346..9d7e6b1ca5b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json index 7ce6a9dd1ae..a73407d0bb8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.77, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json index dbbff620bf4..6b058b84507 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json index 8f1dfac20f0..223095836df 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json index d24a92228d1..34d7cd3fbf2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 11.393, "adomain": [ ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json index 40de103c524..0725b710372 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id_banner", "impid": "imp_id_banner", + "exp": 300, "price": 11.393, "adomain": [], "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json index 8c5b4ea599e..ad84acca12c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json index c34b9f61946..e7b10ebdda4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "cid": "test_cid", "crid": "test_banner_crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json index 81da7b92978..d28852c4ccd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json index 0c6bd826b02..b2442775de6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "mtype": 1, "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json b/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json index ce119ca7efd..2b9d8f83ca4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json index 5de88ba03df..930aa7c8b06 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json index c71dd2560c2..3795c5a9c62 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 6d2e3cf823c..552b0f3a934 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -6,6 +6,7 @@ { "id": "imp_id", "impid": "imp_id", + "exp": 300, "price": 2.942808, "adm": "
", "adid": "94395500", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json index b3b8e50e406..ce65aba9a9b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "
test
", "adid": "imp_id", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json index 16a9acaefd1..f10ab8e286e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json index aadd13302aa..3dd393badc9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 5.0, "adm": "adm4", "crid": "crid4", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json index 8cb45ddcbac..4aa8c6d0985 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 6.0, "adm": "adm4", "crid": "crid4", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json new file mode 100644 index 00000000000..664693ffa74 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json @@ -0,0 +1,27 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "escalax": { + "accountId": "testAccountId", + "sourceId": "testSourceId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json new file mode 100644 index 00000000000..7f2babf609e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + } + } + ], + "seat": "escalax", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "escalax": "{{ escalax.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json new file mode 100644 index 00000000000..e0c6fddd7c7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json @@ -0,0 +1,53 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json b/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json index 1d694702c90..d0043247edf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json new file mode 100644 index 00000000000..67db491fe86 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "felixads": { + "partnerName": "someUniquePartnerName", + "seat": "someSeat", + "token": "someToken" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json new file mode 100644 index 00000000000..0b63bd03f57 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "mediaType": "video", + "origbidcpm": 3.33, + "prebid": { + "type": "video" + } + } + } + ], + "seat": "felixads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "felixads": "{{ felixads.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json new file mode 100644 index 00000000000..9342b97a4fa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "partnerName": "someUniquePartnerName", + "seat": "someSeat", + "token": "someToken" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json new file mode 100644 index 00000000000..ecfbbed0ded --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "ext": { + "mediaType": "video" + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json new file mode 100644 index 00000000000..00822b5da93 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "filmzie": { + "host": "test.host", + "publisherId": "123456" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json new file mode 100644 index 00000000000..e2c3508d1ab --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json @@ -0,0 +1,34 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "filmzie", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "filmzie": "{{ filmzie.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json new file mode 100644 index 00000000000..8e58e53ba4b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json @@ -0,0 +1,40 @@ +{ + "id": "request_id-imp_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json new file mode 100644 index 00000000000..04d26e04318 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json @@ -0,0 +1,15 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json b/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json index 4246d1a5016..e055b56bb65 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 11.393, "adm": "some adm price 10", "adomain": [ diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json index a6d946f403d..da840110dcf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json @@ -6,6 +6,7 @@ { "id": "183599115", "impid": "imp_id", + "exp": 300, "price": 12.34, "adm": "creativeContent", "crid": "81325690", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json index 834a4af5e9d..d583e32cc4f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json @@ -6,6 +6,7 @@ { "id": "12345_freewheelssp-test_1", "impid": "imp-1", + "exp": 1500, "price": 1.0, "adid": "7857", "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json index 13708ad16b1..78b3f939511 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json @@ -38,10 +38,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json index fb69a968abd..683c42863d0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json index ca56cb19b7e..ce0e41775aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "29681110", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json index c88d409c2c2..4d6ea3f9060 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json index 28cf6da40f7..8f2d2e4407c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json @@ -6,12 +6,17 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { "origbidcpm": 3.33, "prebid": { - "type": "banner" + "type": "banner", + "meta": { + "mediaType": "banner", + "adaptercode": "adaptercode" + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json index 04d26e04318..7dc412239c2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json @@ -7,7 +7,15 @@ "id": "bid_id", "impid": "imp_id", "price": 3.33, - "crid": "creativeId" + "crid": "creativeId", + "ext": { + "prebid": { + "meta": { + "mediaType": "banner", + "adaptercode": "adaptercode" + } + } + } } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json index dfa745e2d1d..7dc036ee1d4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json @@ -8,9 +8,26 @@ "mimes" ], "w": 300, - "h": 250 + "h": 250, + "placement": 1 }, "ext": { + "prebid": { + "imp": { + "generic": { + "pmp": { + "deals": [ + { + "id": "dealId" + } + ] + }, + "ext": { + "someExt": "someExt" + } + } + } + }, "generic": { "accountId": 2001, "siteId": 3001, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json index 552408995c8..4ee1ff6a6c8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json @@ -6,6 +6,7 @@ { "id": "bid001", "impid": "impId001", + "exp": 1500, "price": 2.997, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json index 1b5c1802325..a6d65dfcae0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json @@ -36,7 +36,8 @@ "origbidcpm": 3.33 } }, - "aid": "tid" + "aid": "tid", + "ttlseconds" : 1500 } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json index 697daed2ae1..754ed9ddfff 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json @@ -1,20 +1,31 @@ { "id" : "tid", "imp" : [ { - "id" : "impId001", - "video" : { - "mimes" : [ "mimes" ], - "w" : 300, - "h" : 250 + "id": "impId001", + "video": { + "placement": 1, + "mimes": [ + "mimes" + ], + "w": 300, + "h": 250 }, - "secure" : 1, - "ext" : { - "tid" : "${json-unit.any-string}", - "bidder" : { - "accountId" : 2001, - "siteId" : 3001, - "zoneId" : 4001 - } + "pmp": { + "deals": [ + { + "id": "dealId" + } + ] + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + }, + "someExt": "someExt" } } ], "site" : { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json b/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json index 7ab5cf7f347..505a3ca4b5c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json index 4554400f4b1..728dccb2b13 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json b/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json index df8a43a904f..7d19f2a3f5c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json index a71711d597d..f6a034ca8a3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json index 9c2842a59b6..95c6f56f2cc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json @@ -19,8 +19,6 @@ "buyeruid": "GUM-UID" }, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json index 682e43e4516..7c131a1f180 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json index b02e2e91a38..4ac2be5d032 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json @@ -43,9 +43,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json index 95248ad02c0..2068eb254c5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json index c576adccb02..931449bc021 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json index c576adccb02..931449bc021 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json index c576adccb02..931449bc021 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json index 1ad60895c93..3f3b9084e45 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json index 3e1b8422400..05a00845f55 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json index 2b08c0e9f75..dee5e9c6ebb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 250, "w": 300, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json index 621a43422cf..2fcaffd7d5b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 300, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 250, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json index aa5fa9e4250..78e93b06659 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json index dde9fbcb4ea..3da566123a6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 1280, "w": 720, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json index 22f804c5841..8253f6b3e62 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 350, "w": 400, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json index 898142d244c..b67ae7292e1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 350, "w": 400, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json index a32f2cdc011..928b8e37d12 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 1280, "w": 720, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json index 048960f1312..c59dbe20672 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 300, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 250, "nurl": "" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json index 84bfd612d2e..2d7403f3e7e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json index 84bfd612d2e..2d7403f3e7e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json index edf5ff6ca1e..72686361101 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json index aa9c0cccf38..9425d23926c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json index dde2af86099..16f6247161f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 1280, "w": 720, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json index e5e6af0ffa2..e37c977df53 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json b/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json index 42b87323685..8eae0a3d518 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.77, "adm": "adm001", "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json b/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json index 4bfd0bcee03..9ea7cb8766f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json index 2ed506e5a4c..a826a40c221 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json index b79274a221e..05c99b710fc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json @@ -41,13 +41,6 @@ "ext": { "ConsentedProvidersSettings": { "consented_providers": "1~10.20.90" - }, - "consented_providers_settings": { - "consented_providers": [ - 10, - 20, - 90 - ] } } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json b/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json index c561c6a98e8..6361cafe796 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json b/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json index d5d69df907e..55bd555ed6b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json index 0b00509195e..d2d1bd207fa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json @@ -6,11 +6,13 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", "cid": "cid001", "crid": "crid001", + "mtype": 1, "w": 300, "h": 250, "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json index e291739474c..2769168e6ed 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json @@ -11,10 +11,11 @@ "crid": "crid001", "cid": "cid001", "adm": "adm001", + "mtype": 1, "h": 250, "w": 300 } ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json b/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json index 174c7a0894b..3100814d919 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 10, "adomain": [ ], @@ -33,4 +34,4 @@ "auctiontimestamp": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json b/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json index 9255d5d9323..9a0fc145939 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json index c07c5d21b49..4322850fa9d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.3, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json index 76ef74b808d..3fc5c87e169 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json index 4a6e48aac57..b73fb55b074 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json index 0c46e680fbc..1fc85c3cd79 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json @@ -8,6 +8,7 @@ "h": 250 }, "ext": { + "ae": 1, "ix": { "siteId": "10002" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json index 8685c8ede1a..1ecdc0136a1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6", "crid": "crid6", @@ -27,7 +28,47 @@ "ix": "{{ ix.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 0 + "auctiontimestamp": 0, + "fledge": { + "auctionconfigs": [ + { + "impid": "imp_id", + "bidder": "ix", + "adapter": "ix", + "config": { + "seller": "https://test.casalemedia.com", + "decisionLogicUrl": "https://test.casalemedia.com/decision-logic.js", + "trustedScoringSignalsURL": "https://test.casalemedia.com/123", + "interestGroupBuyers": [ + "https://test.com" + ], + "sellerSignals": { + "callbackURL": "https://test.casalemedia.com/callback/1", + "debugURL": "https://test.casalemedia.com/debug/1", + "width": 300, + "height": 250 + }, + "sellerTimeout": 150, + "perBuyerSignals": { + "https://test.com": [ + { + "key": "value" + } + ] + }, + "perBuyerCurrencies": { + "*": "USD" + }, + "sellerCurrency": "USD", + "requestedSize": { + "width": 300, + "height": 250 + }, + "maxTrustedBiddingSignalsURLLength": 1000 + } + } + ] + } }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json index 0658f90c813..ef303b14b8e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json @@ -15,6 +15,7 @@ "h": 250 }, "ext": { + "ae": 1, "tid": "${json-unit.any-string}", "bidder": { "siteId": "10002" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json index 9d9d7035ed7..c62e7c626b9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json @@ -13,5 +13,43 @@ ], "seat": "seatId6" } - ] + ], + "ext": { + "protectedAudienceAuctionConfigs": [ + { + "bidId": "imp_id", + "config": { + "seller": "https://test.casalemedia.com", + "decisionLogicUrl": "https://test.casalemedia.com/decision-logic.js", + "trustedScoringSignalsURL": "https://test.casalemedia.com/123", + "interestGroupBuyers": [ + "https://test.com" + ], + "sellerSignals": { + "callbackURL": "https://test.casalemedia.com/callback/1", + "debugURL": "https://test.casalemedia.com/debug/1", + "width": 300, + "height": 250 + }, + "sellerTimeout": 150, + "perBuyerSignals": { + "https://test.com": [ + { + "key": "value" + } + ] + }, + "perBuyerCurrencies": { + "*": "USD" + }, + "sellerCurrency": "USD", + "requestedSize": { + "width": 300, + "height": 250 + }, + "maxTrustedBiddingSignalsURLLength": 1000 + } + } + ] + } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json index 48fed77c3e7..31a01fdf368 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json index f9c2484affa..196ed6b59e7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json index 10481c0accb..2589f6a4a4d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json index 0c485b9fd14..db43a190b8b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json @@ -38,10 +38,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json index 6b513b46072..aaccad9be2d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json index 668b51a85a8..5cec9fef037 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json index 046aeaa3005..07da2713c33 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json index 25228273c75..56f25641c69 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json index a21706f9abf..59dc0706b2d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json b/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json index 999b4184dbd..e90d9b5f6aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adid": "adid001", "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json index 409ecdcd328..5bf9bad0853 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json index 1036e1e84d0..200e13238f8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json index 0433389b511..d1ae959bebe 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.35, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json b/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json index 5079a616a00..f5503cf319d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json index dd7aa14d18c..4319787685b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json index ad5f545a942..bbff1db6fc9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json @@ -9,7 +9,9 @@ }, "ext": { "loopme": { - "accountId": "testAccountId" + "publisherId": "testPublisherId", + "bundleId": "testBundleId", + "placementId": "testPlacementId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json index 2c86435fb5b..30397fe4de4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json index 67e2f510ea6..d669ee5881f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json @@ -3,15 +3,17 @@ "imp": [ { "id": "imp_id", - "secure": 1, "banner": { "w": 300, "h": 250 }, + "secure": 1, "ext": { "tid": "${json-unit.any-string}", "bidder": { - "accountId": "testAccountId" + "publisherId": "testPublisherId", + "bundleId": "testBundleId", + "placementId": "testPlacementId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json b/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json index 80cbbdf2ebb..58b5b47ee41 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json index a7c3e9ba1e2..194301eae99 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json index dacba278abe..28e33d7e084 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json @@ -6,6 +6,7 @@ { "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 2.734, "adm": "", "adomain": [ diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json index c7c4540bd47..1c31dddaea0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json index fd8cbf0a699..111e147d710 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json index e1f24125c13..08104541960 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json @@ -17,7 +17,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json index dad8c29bdf5..a6bccc7525a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json index 8bd2fdc004e..b2536b23494 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.35, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json index 5c5f50670a2..7eb299b660f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json index 14ebb3b62f8..55b8dba2496 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json @@ -6,6 +6,7 @@ { "id": "randomid", "impid": "test-imp-id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json new file mode 100644 index 00000000000..16518953d21 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 10, + "bidfloorcur": "USD", + "ext": { + "melozen": { + "pubId": "publisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json new file mode 100644 index 00000000000..a810ba066a1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "melozen", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "melozen": "{{ melozen.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json new file mode 100644 index 00000000000..c7a91804b4f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 10, + "bidfloorcur": "USD", + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "pubId": "publisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json new file mode 100644 index 00000000000..93d9130a19b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json @@ -0,0 +1,22 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json new file mode 100644 index 00000000000..d1393d6a4fa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json @@ -0,0 +1,27 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "metax": { + "publisherId": 123, + "adunit": 456 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json new file mode 100644 index 00000000000..82cc50b31be --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "metax", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "metax": "{{ metax.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json new file mode 100644 index 00000000000..eccf15de7bd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json @@ -0,0 +1,60 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "publisherId": 123, + "adunit": 456 + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json index 89dc1790afe..f2c474641aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.5, "nurl": "nurl", "adm": "some-test-ad", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json index 217cac91e56..d24c8274ed9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json index 898532edc4b..10cdbd0a6f5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "banner_imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "29681110", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json new file mode 100644 index 00000000000..cacda5b188c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "missena": { + "apiKey": "apiKey", + "placement": "placement", + "test": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json new file mode 100644 index 00000000000..28f9caabf7e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "request_id", + "impid": "imp_id", + "exp": 300, + "price": 10.2, + "adm": "adm", + "crid": "id", + "ext": { + "origbidcpm": 10.2, + "origbidcur": "USD", + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "missena", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "missena": "{{ missena.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json new file mode 100644 index 00000000000..aa398c49b22 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json @@ -0,0 +1,10 @@ +{ + "request_id": "request_id", + "timeout": 2000, + "referer": "http://www.example.com", + "referer_canonical": "www.example.com", + "consent_string": "", + "consent_required": false, + "placement": "placement", + "test": "test" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json new file mode 100644 index 00000000000..4dece3dba9b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json @@ -0,0 +1,6 @@ +{ + "requestId": "id", + "cpm": 10.2, + "ad": "adm", + "currency": "USD" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json index 1c1fbe7791b..fb8a20ae2b0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "", "cid": "test_cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json index d4ff118ad3c..3f22980426a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adm": "hi", "cid": "test_cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json index 9b41b090e34..63fb79a49a8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json @@ -33,10 +33,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json b/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json index 17ba2b420b7..fba432c5be4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json index bfd9600aa75..1e4f6f4a489 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json @@ -11,7 +11,7 @@ "crid": "crid1", "w": 300, "h": 250, - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", @@ -68,7 +68,7 @@ "crid": "crid1", "w": 300, "h": 250, - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", @@ -128,7 +128,7 @@ "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", "cid": "958", "crid": "29681110", - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", @@ -179,7 +179,7 @@ "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", "cid": "958", "crid": "69595837", - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json index 2d951abdbab..770ee7c9d39 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json @@ -32,7 +32,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=21521324&a=5001&aid=tid&ts=1000&bidder=generic&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "json", @@ -68,7 +69,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=genericAlias&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "json", @@ -102,7 +104,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=generic&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "json", @@ -138,7 +141,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=222214214214&a=5001&aid=tid&ts=1000&bidder=genericAlias&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "xml", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json index 71068f2cb94..be5cf8b9277 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "mtype": 1, "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json index fc7edb8ce86..ebeea62aba0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json index f6a94e868a8..315114b4f75 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json b/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json index 80373e03cb7..4496bc2e4b6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json index 53761fd9c76..a996e780237 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 5.78, "adm": "adm00", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json index 8ffbc819833..0c22b5e89b9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 5.78, "adm": "adm00", "crid": "crid00", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json index fac8f6e765c..d7f4efb8bef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json @@ -45,9 +45,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "delDomain": "se-demo-d.openx.net", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json index 2c7b4e4f22c..91f9dc59e5c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json new file mode 100644 index 00000000000..0b738e1b9d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "oraki": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json new file mode 100644 index 00000000000..4bbd63fe482 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "oraki", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "oraki": "{{ oraki.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json index 1a260bfa518..40a02aff7f5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json @@ -16,6 +16,7 @@ }, "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "mtype": 1 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json b/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json index 6c19a2a44b7..cb7fbc34fe8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json new file mode 100644 index 00000000000..cf500a4f09c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "ownadx": { + "tokenId": "testTokenId", + "seatId": "testSeatId", + "sspId": "testSspId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json new file mode 100644 index 00000000000..d7a74ef6d6d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "ownadx", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "ownadx": "{{ ownadx.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json new file mode 100644 index 00000000000..191c8b5661a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "tokenId": "testTokenId", + "seatId" : "testSeatId", + "sspId" : "testSspId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json new file mode 100644 index 00000000000..9eef9d710d7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype" : 1, + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json index 7a0cf377706..b0995267c08 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json index 79f17402a81..82e6a8182df 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json index f2c7121be84..303e8a7826c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json index 1c26c09c8c7..8bfc8b3f515 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json b/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json index 2b363db2f29..c26bf519990 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json index 4ce766dfbcc..3a20b08ce67 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json @@ -49,9 +49,7 @@ ], "tmax": 5000, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json index 53731b03bf0..4df8cf0c723 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "test-imp-id", + "exp": 1500, "price": 4.75, "adm": "adm9", "crid": "crid9", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json index 7a4f1e0279d..95f031b554b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json @@ -12,7 +12,7 @@ "h": 600 }, "tagid": "slot9", - "bidfloor" : 0.12, + "bidfloor": 0.12, "ext": { "ae": 1, "pmZoneId": "Zone1,Zone2", @@ -45,9 +45,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json index 7b7334c52fe..d263e2b1f43 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json new file mode 100644 index 00000000000..ae786238146 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "pubrise": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json new file mode 100644 index 00000000000..752b5519b40 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "pubrise", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "pubrise": "{{ pubrise.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json index 332e1cae3ac..88c15aebf71 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.75, "adm": "adm8", "crid": "crid8", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json index 4bec8bff353..26809e2c670 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json @@ -42,9 +42,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json index f41e6383c9d..21d6ddcd11e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json @@ -41,10 +41,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json index 0f38f603a90..b6c810ede52 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json new file mode 100644 index 00000000000..e75dfb6e4e0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "qt": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json new file mode 100644 index 00000000000..16db7e67c68 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "qt", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "qt": "{{ qt.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json b/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json index 101455149d7..06a8c3170aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json index 9e1e479b4ed..d3531b49cca 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 5.78, "adm": "adm", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json index c3de29e49d4..f595ea39c9f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json index a2acc3d4df5..4e6047c3738 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "