diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml index 286b03d03d3..39964eb69aa 100644 --- a/.github/workflows/docker-image-publish.yml +++ b/.github/workflows/docker-image-publish.yml @@ -55,11 +55,18 @@ jobs: 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/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/application-settings.md b/docs/application-settings.md index c51febaea3e..39fd52c7e5a 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -259,6 +259,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/developers/code-reviews.md b/docs/developers/code-reviews.md index 78728fef18a..cc8ed667849 100644 --- a/docs/developers/code-reviews.md +++ b/docs/developers/code-reviews.md @@ -43,3 +43,4 @@ 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. diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index bd14db698fc..33a26a74d4b 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT ../../extra/pom.xml @@ -40,6 +40,11 @@ pb-richmedia-filter ${project.version} + + org.prebid.server.hooks.modules + pb-response-correction + ${project.version} + diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 7e3679cabb6..e82a3d761ca 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT confiant-ad-quality 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..d4b39a8214e 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 @@ -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 713641e0442..4a4fcfa80da 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT fiftyone-devicedetection 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/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 4a07feabcd8..6eac58c5ba0 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT ortb2-blocking 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..ff9e6ab6c3c 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 @@ -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/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 a66f94ac52c..fe1ea6e9614 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 @@ -36,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) @@ -50,10 +53,10 @@ public class Ortb2BlockingBidderRequestHookTest { .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private BidderCatalog bidderCatalog; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private BidRejectionTracker bidRejectionTracker; private Ortb2BlockingBidderRequestHook hook; @@ -68,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", bidRejectionTracker, null, true)); + BidderInvocationContextImpl.of( + "bidder1", + Map.of("bidder1", "bidder1Base"), + bidRejectionTracker, + null, + true)); // then assertThat(result.succeeded()).isTrue(); @@ -258,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/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java index d11d1912598..8b68c9279df 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,6 +1,7 @@ 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; @@ -9,6 +10,8 @@ import org.prebid.server.execution.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; @@ -39,6 +42,28 @@ public static BidderInvocationContext of(String bidder, 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) diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml new file mode 100644 index 00000000000..a84008ae0d4 --- /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.13.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..3df769e52cb --- /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,137 @@ +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.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("<\\w*VAST\\w+", 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 = 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..cea7a80b131 --- /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.modules.pb.response.correction.v1.model.InvocationResultImpl; +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.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-hook"; + + 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/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..1a39413583c --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.response.correction.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 org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +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/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..537e79943cd --- /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(" 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 97f3159d5cd..4bf4b87f902 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 64def14267f..b7eac66e702 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT ../../extra/pom.xml @@ -21,6 +21,7 @@ confiant-ad-quality pb-richmedia-filter fiftyone-devicedetection + pb-response-correction diff --git a/extra/pom.xml b/extra/pom.xml index b3ec8b88bc8..25987cb8260 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT pom @@ -49,9 +49,10 @@ 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.9.1 @@ -212,6 +213,11 @@ geoip2 ${maxmind-client.version} + + software.amazon.awssdk + s3 + ${aws.awssdk.version} + com.google.protobuf protobuf-java diff --git a/pom.xml b/pom.xml index f47f59fb1cf..5d8987c87bd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.13.0-SNAPSHOT extra/pom.xml @@ -170,6 +170,10 @@ org.postgresql postgresql + + software.amazon.awssdk + s3 + com.github.ben-manes.caffeine caffeine @@ -328,6 +332,11 @@ mysql test + + org.testcontainers + localstack + test + org.testcontainers postgresql 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/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/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/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java index f6674bca659..dc9143966f3 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -12,6 +12,7 @@ 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; @@ -34,6 +35,7 @@ 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; @@ -43,39 +45,29 @@ 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.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; import java.util.zip.GZIPOutputStream; -public class AgmaAnalyticsReporter implements AnalyticsReporter { +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 httpTimeoutMs; - private final long maxBufferSize; - private final long maxEventCount; - private final long bufferTimeoutMs; + + 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 ReentrantLock lockOnSend; - private final AtomicReference> events; private final MultiMap headers; - private final AtomicLong byteSize; - private volatile long reportTimerId; public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, PrebidVersionProvider prebidVersionProvider, @@ -90,20 +82,21 @@ public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, this.httpTimeoutMs = agmaAnalyticsProperties.getHttpTimeoutMs(); this.compressToGzip = agmaAnalyticsProperties.isGzip(); - this.maxBufferSize = agmaAnalyticsProperties.getBufferSize(); - this.maxEventCount = agmaAnalyticsProperties.getMaxEventsCount(); - this.bufferTimeoutMs = agmaAnalyticsProperties.getBufferTimeoutMs(); + 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.lockOnSend = new ReentrantLock(); - this.events = new AtomicReference<>(new ConcurrentLinkedQueue<>()); this.headers = makeHeaders(Objects.requireNonNull(prebidVersionProvider)); - this.byteSize = new AtomicLong(); - this.reportTimerId = setBufferTimer(); + } + + @Override + public void initialize(Promise initializePromise) { + vertx.setPeriodic(1000L, ignored -> sendEvents(buffer.pollAll())); + initializePromise.complete(); } @Override @@ -149,9 +142,12 @@ public Future processEvent(T event) { Instant.ofEpochMilli(timeoutContext.getStartTime()), clock.getZone())) .build(); - buffer(agmaEvent); - sendEventsOnCondition(byteSize -> byteSize.get() > maxBufferSize, byteSize); - sendEventsOnCondition(eventsReference -> eventsReference.get().size() > maxEventCount, events); + final String eventString = jacksonMapper.encodeToString(agmaEvent); + buffer.put(eventString, eventString.length()); + final List toFlush = buffer.pollToFlush(); + if (!toFlush.isEmpty()) { + sendEvents(toFlush); + } return Future.succeededFuture(); } @@ -205,37 +201,8 @@ private static String getPublisherId(BidRequest bidRequest) { return publisherId; } - private void buffer(T event) { - final String jsonEvent = jacksonMapper.encodeToString(event); - events.get().add(jsonEvent); - byteSize.getAndAdd(jsonEvent.getBytes().length); - } - - private boolean sendEventsOnCondition(Predicate conditionToSend, T conditionValue) { - boolean requestWasSent = false; - if (conditionToSend.test(conditionValue)) { - lockOnSend.lock(); - try { - if (conditionToSend.test(conditionValue)) { - requestWasSent = true; - sendEvents(events); - } - } catch (Exception exception) { - logger.error("[agmaAnalytics] Failed to send analytics report to endpoint {} with a reason {}", - url, exception.getMessage()); - } finally { - lockOnSend.unlock(); - } - } - return requestWasSent; - } - - private void sendEvents(AtomicReference> events) { - final Queue copyToSend = events.getAndSet(new ConcurrentLinkedQueue<>()); - - resetReportEventsConditions(); - - final String payload = preparePayload(copyToSend); + private void sendEvents(List events) { + 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); @@ -243,13 +210,7 @@ private void sendEvents(AtomicReference> events) { responseFuture.onComplete(this::handleReportResponse); } - private void resetReportEventsConditions() { - byteSize.set(0); - vertx.cancelTimer(reportTimerId); - reportTimerId = setBufferTimer(); - } - - private static String preparePayload(Queue events) { + private static String preparePayload(List events) { return "[" + String.join(",", events) + "]"; } @@ -279,17 +240,6 @@ private void handleReportResponse(AsyncResult result) { } } - private long setBufferTimer() { - return vertx.setTimer(bufferTimeoutMs, timerId -> sendOnTimer()); - } - - private void sendOnTimer() { - final boolean requestWasSent = sendEventsOnCondition(events -> !events.get().isEmpty(), events); - if (!requestWasSent) { - setBufferTimer(); - } - } - private MultiMap makeHeaders(PrebidVersionProvider versionProvider) { final MultiMap headers = MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) 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/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index b843b7f07e4..567f73e63de 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -122,6 +122,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; @@ -1325,13 +1327,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(); @@ -1552,16 +1552,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 +1568,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 +1575,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 +1584,6 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, */ private Map keywordsCreatorByBidType( ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1602,21 +1599,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 +1621,6 @@ BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, ac } private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targeting, - boolean isApp, JsonNode priceGranularity, BidRequest bidRequest, Account account, @@ -1632,13 +1628,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, diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 8ccada983ac..2dbf1a6528f 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -50,6 +50,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; @@ -296,7 +297,7 @@ private Future runAuction(AuctionContext receivedContext) { .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( @@ -470,44 +471,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, @@ -546,9 +515,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); } @@ -564,21 +530,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, @@ -631,10 +582,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 @@ -645,9 +592,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(); @@ -676,13 +620,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, @@ -734,9 +671,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() @@ -744,10 +678,6 @@ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions, String bidder) { final List allowedBidders = eidPermissions.get(source); return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() @@ -755,9 +685,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, @@ -810,9 +737,6 @@ private static Map bidderToPrebidBidders(BidRequest bidRequest return bidderToPrebidParameters; } - /** - * Returns {@link AuctionParticipation} for the given bidder. - */ private AuctionParticipation createAuctionParticipation( BidderPrivacyResult bidderPrivacyResult, Map> impBidderToStoredBidResponse, @@ -1237,16 +1161,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.REQUEST_BLOCKED_UNSUPPORTED_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()) @@ -1257,6 +1185,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) + .rejectAll(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() @@ -1416,13 +1372,6 @@ private List validateAndAdjustBids(List - * 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) { @@ -1483,13 +1432,6 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati 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()) { @@ -1594,13 +1536,6 @@ 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) { @@ -1649,9 +1584,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; 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/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index 70fd0244bc5..e916ee0b3a5 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -58,6 +58,11 @@ public enum BidRejectionReason { */ 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. 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/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/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/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/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/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/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/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java index 9a6416ba44c..b841bf8a136 100644 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java @@ -1,5 +1,6 @@ package org.prebid.server.execution; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; @@ -69,12 +70,14 @@ public RemoteFileSyncer(RemoteFileProcessor processor, getFileRequestOptions = new RequestOptions() .setMethod(HttpMethod.GET) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); isUpdateRequiredRequestOptions = new RequestOptions() .setMethod(HttpMethod.HEAD) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); } private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { @@ -112,8 +115,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 +150,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 +162,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/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/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/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/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/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java new file mode 100644 index 00000000000..f6198a5ad94 --- /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; +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/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/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 1006403c9c4..f7aaa9bb4ba 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -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/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/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/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/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/bizzclick.yaml b/src/main/resources/bidder-config/bizzclick.yaml deleted file mode 100644 index f5037c1014a..00000000000 --- a/src/main/resources/bidder-config/bizzclick.yaml +++ /dev/null @@ -1,15 +0,0 @@ -adapters: - bizzclick: - endpoint: http://{{Host}}.bizzclick.com/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} - meta-info: - maintainer-email: support@bizzclick.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/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/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/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 5a5cc21f466..b198a837a9c 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -2,7 +2,7 @@ adapters: freewheelssp: endpoint: https://ads.stickyadstv.com/openrtb/dsp ortb-version: "2.6" - modifyingVastXmlAllowed: true + 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 b52c7ac21b1..2c15fd531dd 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -114,9 +114,9 @@ adapters: - video - native site-media-types: - - banner - - video - - native + - banner + - video + - native supported-vendors: vendor-id: 263 usersync: diff --git a/src/main/resources/bidder-config/kargo.yaml b/src/main/resources/bidder-config/kargo.yaml index c4e3f78d8e8..56d5e9ea22d 100644 --- a/src/main/resources/bidder-config/kargo.yaml +++ b/src/main/resources/bidder-config/kargo.yaml @@ -3,7 +3,7 @@ adapters: 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/limelightDigital.yaml b/src/main/resources/bidder-config/limelightDigital.yaml index f13e008afd0..16d495c8d55 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -36,6 +36,8 @@ adapters: embimedia: enabled: false endpoint: http://ads-pbs.bidder-embi.media/openrtb/{{PublisherID}}?host={{Host}} + tgm: + enabled: false meta-info: maintainer-email: engineering@project-limelight.com app-media-types: 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/rubicon.yaml b/src/main/resources/bidder-config/rubicon.yaml index 38a3e2d24aa..882732f83a5 100644 --- a/src/main/resources/bidder-config/rubicon.yaml +++ b/src/main/resources/bidder-config/rubicon.yaml @@ -38,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/sovrn.yaml b/src/main/resources/bidder-config/sovrn.yaml index d2d22c55c21..bad733681a6 100644 --- a/src/main/resources/bidder-config/sovrn.yaml +++ b/src/main/resources/bidder-config/sovrn.yaml @@ -1,7 +1,7 @@ adapters: sovrn: endpoint: http://ap.lijit.com/rtb/bid?src=prebid_server - 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/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/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/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/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/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/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index eedb8412ba5..5efcdf40709 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,9 @@ 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") @JsonValue final String code 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/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 11173093f85..0d8333b3375 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,6 +7,7 @@ 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-hook"), ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") 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..6e09273bef7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy @@ -0,0 +1,62 @@ +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.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingActionOverride { + + List enforceBlocks + List blockedAdomain + List blockedApp + List blockedBannerAttr + List blockedAdvCat + List blockedBannerType + + List blockUnknownAdomain + List blockUnknownAdvCat + + List allowedAdomainForDeals + List allowedAppForDeals + List allowedBannerAttrForDeals + 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 BATTR: + blockedBannerAttr = blocked + allowedBannerAttrForDeals = 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 index 827559c17cd..e1688e2d2b3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy @@ -1,13 +1,15 @@ package org.prebid.server.functional.model.config -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming +import com.fasterxml.jackson.annotation.JsonValue import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) -class Ortb2BlockingAttribute { +enum Ortb2BlockingAttribute { - Boolean enforceBlocks - List blockedAdomain + BADV, BAPP, BATTR, BCAT, BTYPE + + @JsonValue + String getValue() { + name().toLowerCase() + } } 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..5c405269466 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy @@ -0,0 +1,64 @@ +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.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingAttributeConfig { + + Boolean enforceBlocks + + Object blockedAdomain + Object blockedApp + Object blockedBannerAttr + Object blockedAdvCat + Object blockedBannerType + + Object blockUnknownAdomain + Object blockUnknownAdvCat + + Object allowedAdomainForDeals + Object allowedAppForDeals + Object allowedBannerAttrForDeals + 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 BATTR: + blockedBannerAttr = ortb2Attributes + allowedBannerAttrForDeals = 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 index fbbe08089fc..1cef82cbe52 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -1,11 +1,9 @@ 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) class Ortb2BlockingConfig { - Ortb2BlockingAttributes attributes + 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/Ortb2BlockingAttributes.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy similarity index 66% rename from src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy rename to src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy index e5a3c13f5d9..46af75deac6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy @@ -5,7 +5,9 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) -class Ortb2BlockingAttributes { +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class PbResponseCorrection { - Ortb2BlockingAttribute badv + 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 74a6ddab94d..f9121ae0b3a 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 @@ -11,4 +11,5 @@ class PbsModulesConfig { RichmediaFilter pbRichmediaFilter Ortb2BlockingConfig ortb2Blocking + PbResponseCorrection pbResponseCorrection } 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/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index 3f14bac3db1..79cf8ad9317 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 @@ -14,6 +14,7 @@ enum BidRejectionReason { 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), 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/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/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/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/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/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index 8b087120db0..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,19 +17,20 @@ 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 @@ -37,11 +39,15 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha 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 @@ -58,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 @@ -84,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 @@ -212,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) @@ -247,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) @@ -289,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"() { @@ -395,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 @@ -512,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] @@ -531,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 @@ -565,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" @@ -600,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."] @@ -646,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 } @@ -662,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) } @@ -805,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/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 6ffaedca01e..7eb388cf202 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -561,6 +561,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber] } } @@ -584,6 +585,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber, PBSUtils.randomNumber] } } 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..67ff7907901 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -30,10 +30,12 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor 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 +670,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 +696,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 +1102,25 @@ 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 + } + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } 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..19cb2cd53de 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 @@ -5,6 +5,7 @@ import org.prebid.server.functional.model.config.ExecutionPlan 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_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 @@ -22,6 +23,12 @@ 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, PB_RESPONSE_CORRECTION, [ALL_PROCESSED_BID_RESPONSES]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getRichMediaFilterSettings(String scriptPattern, boolean filterMraidEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { 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 index d5eec884e9f..5b9d96e6bf3 100644 --- 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 @@ -1,32 +1,1182 @@ 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.bidder.Openx 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.Ortb2BlockingAttributes +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.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +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.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 spock.lang.PendingFeature 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.OPENX import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +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.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.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.ErrorType.GENERIC +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 final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings) + private static final Map OPENX_CONFIG = ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final String WILDCARD = '*' + + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + OPENX_CONFIG) + + def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | 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 = BidRequest.defaultBidRequest.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 | BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + PBSUtils.randomString | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should send only not matched ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should discard unknown adomain bids when enforcement is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should discard bids with deals when allowed ortb2 attribute for deals is not matched"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should be able to override enforcement by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + 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: OPENX)] + } + 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 == OPENX + 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 | BATTR + } + + def "PBS should be able to override enforcement by media type"() { + given: "Default bidRequest" + def bannerImp = Imp.getDefaultImpression(BANNER) + def videoImp = Imp.getDefaultImpression(VIDEO) + def bidRequest = BidRequest.defaultBidRequest.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 bidRequest" + def bannerImp = Imp.getDefaultImpression(BANNER) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [bannerImp] + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], BATTR).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BATTR): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bannerImp, ortb2Attribute, BATTR)])] + } + 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 == bannerImp.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, BATTR) == [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 + } + + @PendingFeature + def "PBS should be able to override enforcement by deal id"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + 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.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 | attributeName + PBSUtils.randomNumber | PBSUtils.randomString | BADV + PBSUtils.randomNumber | PBSUtils.randomString | BAPP + PBSUtils.randomNumber | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + WILDCARD | PBSUtils.randomString | BADV + WILDCARD | PBSUtils.randomString | BAPP + WILDCARD | PBSUtils.randomString | BCAT + WILDCARD | PBSUtils.randomNumber | BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by media type"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should be able to override block unknown adomain by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + + 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: OPENX)] + } + 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.bid.flatten().size() == 1 + assert response.seatbid.first.seat == OPENX + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + + 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: OPENX)] + } + 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.bid.flatten().size() == 1 + assert response.seatbid.first.seat == OPENX + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should use first override when multiple match same condition"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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: [banner]"] + + 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 | 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 | BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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 | BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by media type"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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: [BANNER])) + 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 | BATTR + } + + def "PBS should merge allowed bundle for deals overrides together"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + 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] | BATTR + } + + def "PBS should not be override from config when ortb2 attribute present in incoming request"() { + given: "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: + bidRequest | ortb2Attributes | attributeName + BidRequest.defaultBidRequest.tap { badv = [PBSUtils.randomString] } | PBSUtils.randomString | BADV + BidRequest.defaultBidRequest.tap { bapp = [PBSUtils.randomString] } | PBSUtils.randomString | BAPP + BidRequest.defaultBidRequest.tap { bcat = [PBSUtils.randomString] } | PBSUtils.randomString | BCAT + BidRequest.defaultBidRequest.tap { imp[0].banner.battr = [PBSUtils.randomNumber] } | PBSUtils.randomNumber | BATTR + BidRequest.defaultBidRequest.tap { imp[0].banner.btype = [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 account with return bid status" + given: "Default bidRequest with returnAllBidStatus attribute" def bidRequest = BidRequest.defaultBidRequest.tap { it.ext.prebid.returnAllBidStatus = true } @@ -34,18 +1184,13 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { and: "Default bidder response with aDomain" def aDomain = PBSUtils.randomString def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - it.seatbid.first.bid.first.adomain = [aDomain] + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, aDomain, BADV)] } bidder.setResponse(bidRequest.id, bidResponse) and: "Account in the DB with blocking configuration" - def blockingAttributes = new Ortb2BlockingAttributes(badv: new Ortb2BlockingAttribute(enforceBlocks: true, blockedAdomain: [aDomain])) - def blockingConfig = new Ortb2BlockingConfig(attributes: blockingAttributes) - def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [BIDDER_REQUEST, RAW_BIDDER_RESPONSE]) - def richMediaFilterConfig = new PbsModulesConfig(ortb2Blocking: blockingConfig) - def accountHooksConfig = new AccountHooksConfiguration(executionPlan: executionPlan, modules: richMediaFilterConfig) - def accountConfig = new AccountConfig(hooks: accountHooksConfig) - def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + def attributes = [(BADV): new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockedAdomain: [aDomain])] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) accountDao.save(account) when: "PBS processes the auction request" @@ -55,8 +1200,78 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + 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 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 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 BATTR: + return bidRequest.imp[0].banner.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 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/requestcorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy new file mode 100644 index 00000000000..2c6c4dfd146 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy @@ -0,0 +1,507 @@ +package org.prebid.server.functional.tests.module.requestcorrenction + +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) + + 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("<${PBSUtils.randomString}VAST${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 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("<${PBSUtils.randomString}VAST${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) + 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 << [BANNER, AUDIO, NATIVE] + } + + 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/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/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java index f033bbc475c..2427942605e 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java @@ -10,7 +10,6 @@ import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; -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; @@ -33,7 +32,6 @@ import org.prebid.server.version.PrebidVersionProvider; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; -import org.springframework.test.util.ReflectionTestUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -43,11 +41,9 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Map; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; 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; @@ -56,7 +52,6 @@ import static org.mockito.ArgumentMatchers.anyString; 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; @@ -72,10 +67,10 @@ public class AgmaAnalyticsReporterTest extends VertxTest { private static final TCString PARSED_VALID_CONSENT = TCString.decode(VALID_CONSENT); - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private Vertx vertx; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private HttpClient httpClient; @Mock @@ -106,6 +101,10 @@ public void setUp() { 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); } @@ -151,7 +150,7 @@ public void processEventShouldSendEventWhenEventIsAuctionEvent() { final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; verify(httpClient).request( - eq(HttpMethod.POST), + eq(POST), eq("http://endpoint.com"), headersCaptor.capture(), eq(expectedEventPayload), @@ -207,7 +206,7 @@ public void processEventShouldSendEventWhenEventIsVideoEvent() { final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; verify(httpClient).request( - eq(HttpMethod.POST), + eq(POST), eq("http://endpoint.com"), headersCaptor.capture(), eq(expectedEventPayload), @@ -263,7 +262,7 @@ public void processEventShouldSendEventWhenEventIsAmpEvent() { final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; verify(httpClient).request( - eq(HttpMethod.POST), + eq(POST), eq("http://endpoint.com"), headersCaptor.capture(), eq(expectedEventPayload), @@ -330,7 +329,7 @@ public void processEventShouldSendEventWhenConsentIsValidButWasParsedFromUserExt final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; verify(httpClient).request( - eq(HttpMethod.POST), + eq(POST), eq("http://endpoint.com"), any(), eq(expectedEventPayload), @@ -449,7 +448,7 @@ public void processEventShouldSendEncodingGzipHeaderAndCompressedPayload() { final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; verify(httpClient).request( - eq(HttpMethod.POST), + eq(POST), eq("http://endpoint.com"), headersCaptor.capture(), aryEq(gzip(expectedEventPayload)), @@ -465,207 +464,6 @@ public void processEventShouldSendEncodingGzipHeaderAndCompressedPayload() { assertThat(result.succeeded()).isTrue(); } - @Test - public void processEventShouldSendEventsAndResetSendConditionParameters() { - // given - given(httpClient.request(any(), anyString(), any(), anyString(), anyLong())) - .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null))); - - 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 - target.processEvent(auctionEvent); - - // then - verify(vertx).cancelTimer(anyLong()); - verify(vertx, times(2)).setTimer(eq(10000L), any()); - final AtomicLong byteSize = (AtomicLong) ReflectionTestUtils.getField(target, "byteSize"); - assertThat(byteSize.get()).isEqualTo(0); - final Long currentTimerId = (Long) ReflectionTestUtils.getField(target, "reportTimerId"); - assertThat(currentTimerId).isEqualTo(2); - final AtomicReference> events = (AtomicReference>) ReflectionTestUtils.getField( - target, "events"); - assertThat(events.get()).hasSize(0); - } - - @Test - public void processEventShouldSendEventsWhenTheirSizeIsHigherMaxBufferSize() { - // given - given(httpClient.request(any(), anyString(), any(), anyString(), anyLong())) - .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null))); - - final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() - .url("http://endpoint.com") - .gzip(false) - .bufferSize(300) - .bufferTimeoutMs(10000L) - .maxEventsCount(2) - .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 PrivacyContext privacyContext = PrivacyContext.of( - null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build()); - final TimeoutContext timeoutContext = TimeoutContext.of(clock.millis(), null, 1); - - final AuctionContext auctionContext = AuctionContext.builder() - .privacyContext(privacyContext) - .timeoutContext(timeoutContext) - .bidRequest(BidRequest.builder() - .site(givenSite) - .build()) - .build(); - - final AuctionEvent auctionEvent = AuctionEvent.builder().auctionContext(auctionContext).build(); - final AmpEvent ampEvent = AmpEvent.builder().auctionContext(auctionContext).build(); - final VideoEvent videoEvent = VideoEvent.builder().auctionContext(auctionContext).build(); - - // when - target.processEvent(auctionEvent); - AtomicLong byteSize = (AtomicLong) ReflectionTestUtils.getField(target, "byteSize"); - assertThat(byteSize.get()).isEqualTo(122L); - - target.processEvent(ampEvent); - byteSize = (AtomicLong) ReflectionTestUtils.getField(target, "byteSize"); - assertThat(byteSize.get()).isEqualTo(240); - - target.processEvent(videoEvent); - byteSize = (AtomicLong) ReflectionTestUtils.getField(target, "byteSize"); - assertThat(byteSize.get()).isEqualTo(0); - - // then - final AgmaEvent expectedEvent1 = AgmaEvent.builder() - .eventType("auction") - .accountCode("accountCode") - .site(givenSite) - .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) - .build(); - - final AgmaEvent expectedEvent2 = AgmaEvent.builder() - .eventType("amp") - .accountCode("accountCode") - .site(givenSite) - .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) - .build(); - - final AgmaEvent expectedEvent3 = AgmaEvent.builder() - .eventType("video") - .accountCode("accountCode") - .site(givenSite) - .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) - .build(); - - final String expectedEventPayload = "[" - + jacksonMapper.encodeToString(expectedEvent1) + "," - + jacksonMapper.encodeToString(expectedEvent2) + "," - + jacksonMapper.encodeToString(expectedEvent3) + "]"; - - verify(httpClient).request( - eq(HttpMethod.POST), - eq("http://endpoint.com"), - any(), - eq(expectedEventPayload), - eq(1000L)); - } - - @Test - public void processEventShouldSendEventsWhenTheirCountIsHigherMaxCount() { - // given - given(httpClient.request(any(), anyString(), any(), anyString(), anyLong())) - .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null))); - - final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() - .url("http://endpoint.com") - .gzip(false) - .bufferSize(100000) - .bufferTimeoutMs(10000L) - .maxEventsCount(2) - .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 PrivacyContext privacyContext = PrivacyContext.of( - null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build()); - final TimeoutContext timeoutContext = TimeoutContext.of(clock.millis(), null, 1); - - final AuctionContext auctionContext = AuctionContext.builder() - .privacyContext(privacyContext) - .timeoutContext(timeoutContext) - .bidRequest(BidRequest.builder() - .site(givenSite) - .build()) - .build(); - - final AuctionEvent auctionEvent = AuctionEvent.builder().auctionContext(auctionContext).build(); - final AmpEvent ampEvent = AmpEvent.builder().auctionContext(auctionContext).build(); - final VideoEvent videoEvent = VideoEvent.builder().auctionContext(auctionContext).build(); - - // when - target.processEvent(auctionEvent); - AtomicReference> events = (AtomicReference>) ReflectionTestUtils.getField( - target, "events"); - assertThat(events.get()).hasSize(1); - - target.processEvent(ampEvent); - events = (AtomicReference>) ReflectionTestUtils.getField( - target, "events"); - assertThat(events.get()).hasSize(2); - - target.processEvent(videoEvent); - events = (AtomicReference>) ReflectionTestUtils.getField( - target, "events"); - assertThat(events.get()).hasSize(0); - - // then - final AgmaEvent expectedEvent1 = AgmaEvent.builder() - .eventType("auction") - .accountCode("accountCode") - .site(givenSite) - .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) - .build(); - - final AgmaEvent expectedEvent2 = AgmaEvent.builder() - .eventType("amp") - .accountCode("accountCode") - .site(givenSite) - .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) - .build(); - - final AgmaEvent expectedEvent3 = AgmaEvent.builder() - .eventType("video") - .accountCode("accountCode") - .site(givenSite) - .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) - .build(); - - final String expectedEventPayload = "[" - + jacksonMapper.encodeToString(expectedEvent1) + "," - + jacksonMapper.encodeToString(expectedEvent2) + "," - + jacksonMapper.encodeToString(expectedEvent3) + "]"; - - verify(httpClient).request( - eq(HttpMethod.POST), - eq("http://endpoint.com"), - any(), - eq(expectedEventPayload), - eq(1000L)); - } - private static byte[] gzip(String value) { try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(obj)) { 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/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 13944033ed1..17de10e38b5 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -82,6 +82,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; @@ -1223,7 +1224,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 +1283,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 +1552,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 +1570,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()); } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index ffff9684fb1..0c7710dbc74 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -48,6 +48,8 @@ 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; @@ -329,6 +331,7 @@ public void setUp() { null, null, 0, + null, false, false, CompressionType.NONE, @@ -4668,6 +4671,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::getRejectionReasons) + .isEqualTo(Map.of("impId1", BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY)); + + } + @Test public void shouldConvertBidRequestOpenRTBVersionToConfiguredByBidder() { // given 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/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 2e0e0422450..e2394769585 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java @@ -275,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/privacy/enforcement/CcpaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java index b8287e6c086..5459e232386 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 @@ -76,6 +76,7 @@ public void setUp() { null, null, 0, + null, true, false, null, @@ -213,6 +214,7 @@ public void enforceShouldSkipNoSaleBiddersAndNotEnforcedByBidderConfig() { null, null, 0, + null, false, false, null, 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/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/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/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/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/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/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/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/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/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 8c35236c361..8a55773a0c0 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -646,7 +646,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"), diff --git a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java b/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java index 341c7764cb3..9acd719d30f 100644 --- a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java +++ b/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java @@ -1,5 +1,6 @@ package org.prebid.server.execution; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -182,6 +183,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"); @@ -209,7 +212,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"))); @@ -240,7 +246,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)); @@ -274,8 +283,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 +304,8 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { given(fileSystem.move(anyString(), any(), any(CopyOptions.class))) .willReturn(Future.succeededFuture()); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -354,7 +368,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())) .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture(new RuntimeException()))); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -370,7 +385,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { @Test public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotSet() { // given - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(false)); @@ -382,6 +398,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,6 +413,7 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS verify(fileSystem).open(eq(TMP_FILE_PATH), any()); verify(httpClient).request(any()); verify(asyncFile).close(); + verify(httpClientResponse).statusCode(); verify(remoteFileProcessor).setDataPath(any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); @@ -419,8 +438,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,6 +446,7 @@ 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()); @@ -437,11 +455,81 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { verifyNoInteractions(remoteFileProcessor); } + @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(remoteFileProcessor, never()).setDataPath(any()); + verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); + } + + @Test + public void syncShouldNotUpdateFileWhenServerRespondsWithNonOkStatusCode() { + // given + remoteFileSyncer = new RemoteFileSyncer( + remoteFileProcessor, 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(remoteFileProcessor.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/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 70835b4286a..0990d97b5aa 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -493,7 +493,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 +510,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/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..9f40ed1bc81 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -726,7 +726,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); 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..c2a84bcc922 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -587,7 +587,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); 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/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/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/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/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/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 e9ff0df5dd3..3dd300524c0 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -1,12 +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; @@ -16,14 +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 = 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"; @@ -31,6 +66,24 @@ public class PriceFloorsTest extends IntegrationTest { private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + @BeforeAll + 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 @@ -45,7 +98,7 @@ 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); @@ -54,7 +107,8 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", firstResponse, - singletonList("generic")); + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); // given final StubMapping stubMapping = WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) @@ -65,7 +119,7 @@ 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); @@ -75,7 +129,8 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", secondResponse, - singletonList("generic")); + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); } @Test @@ -87,7 +142,7 @@ 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); @@ -96,6 +151,11 @@ public void openrtb2AuctionShouldSkipPriceFloorsForTheGenericBidderWhenGenericIs assertJsonEquals( "openrtb2/floors/floors-test-auction-response-no-signal.json", firstResponse, - singletonList("generic")); + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); + } + + private static String replaceBidderRelatedStaticInfo(String json, String bidder) { + return IntegrationTestsUtil.replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT); } } 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/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java index bac345c8906..d09df3327a8 100644 --- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java @@ -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/S3ApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java new file mode 100644 index 00000000000..2f7c293f9f8 --- /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; +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/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 863ce3bb53b..a14c6141355 100644 --- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java @@ -394,6 +394,7 @@ private static BidderInfo givenBidderInfo(String aliasOf) { null, null, 0, + null, true, false, CompressionType.NONE, 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/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..e6795976a7f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json @@ -0,0 +1,34 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "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/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 89% 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..9bf200e6d9d 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 @@ -22,14 +22,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/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..fb24eb9368c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "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/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..0aa7a90e2d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "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/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..6871f609875 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "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/rtbhouse/test-auction-rtbhouse-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json index c137b840e4b..841d82b403c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json @@ -7,7 +7,7 @@ "id": "bid_id", "impid": "imp_id", "price": 0.5, - "adm": "some-test-ad", + "adm": "some-test-ad_0.5", "adid": "12345678", "cid": "987", "crid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json index 94da12115c9..9b33d159d6f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json @@ -9,7 +9,7 @@ "impid": "imp_id", "price": 0.500000, "adid": "12345678", - "adm": "some-test-ad", + "adm": "some-test-ad_${AUCTION_PRICE}", "cid": "987", "crid": "12345678", "h": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json new file mode 100644 index 00000000000..82490a56da6 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tgm": { + "host": "test.host", + "publisherId": "123456" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json new file mode 100644 index 00000000000..a312ae577d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json @@ -0,0 +1,33 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "tgm", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "tgm": "{{ tgm.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-request.json new file mode 100644 index 00000000000..8e58e53ba4b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-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/tgm/test-tgm-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-response.json new file mode 100644 index 00000000000..04d26e04318 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-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/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml index ef28a3481be..786b376ffed 100644 --- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml +++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml @@ -124,7 +124,7 @@ accounts: auction: price-floors: fetch: - url: http://localhost:8090/floors-provider + url: http://localhost:8100/floors-provider enabled: true domains: - rubiconproject.com diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 50562212bd7..853cf7c4652 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -64,6 +64,8 @@ adapters.adtelligent.aliases.copper6.enabled=true adapters.adtelligent.aliases.copper6.endpoint=http://localhost:8090/copper6-exchange adapters.adtelligent.aliases.indicue.enabled=true adapters.adtelligent.aliases.indicue.endpoint=http://localhost:8090/indicue-exchange +adapters.adtonos.enabled=true +adapters.adtonos.endpoint=http://localhost:8090/adtonos-exchange/{{PublisherId}} adapters.adtrgtme.enabled=true adapters.adtrgtme.endpoint=http://localhost:8090/adtrgtme-exchange adapters.advangelists.enabled=true @@ -127,8 +129,8 @@ adapters.bidstack.enabled=true adapters.bidstack.endpoint=http://localhost:8090/bidstack-exchange adapters.bigoad.enabled=true adapters.bigoad.endpoint=http://localhost:8090/bigoad-exchange -adapters.bizzclick.enabled=true -adapters.bizzclick.endpoint=http://localhost:8090/bizzclick-exchange?host={{Host}}&source={{SourceId}}&account={{AccountID}} +adapters.blasto.enabled=true +adapters.blasto.endpoint=http://localhost:8090/blasto-exchange?source={{SourceId}}&account={{AccountID}} adapters.bliink.enabled=true adapters.bliink.endpoint=http://localhost:8090/bliink-exchange adapters.bluesea.enabled=true @@ -155,6 +157,8 @@ adapters.coinzilla.enabled=true adapters.coinzilla.endpoint=http://localhost:8090/coinzilla-exchange adapters.consumable.enabled=true adapters.consumable.endpoint=http://localhost:8090/consumable-exchange +adapters.copper6ssp.enabled=true +adapters.copper6ssp.endpoint=http://localhost:8090/copper6ssp-exchange adapters.criteo.enabled=true adapters.criteo.endpoint=http://localhost:8090/criteo-exchange adapters.criteo.generate-slot-id=false @@ -190,6 +194,8 @@ adapters.epom.endpoint=http://localhost:8090/epom-exchange adapters.epsilon.enabled=true adapters.epsilon.endpoint=http://localhost:8090/epsilon-exchange adapters.epsilon.generate-bid-id=false +adapters.escalax.enabled=true +adapters.escalax.endpoint=http://localhost:8090/escalax-exchange?k={{AccountID}}&name={{SourceId}} adapters.evolution.enabled=true adapters.evolution.endpoint=http://localhost:8090/evolution-exchange adapters.evtech.enabled=true @@ -271,6 +277,8 @@ adapters.limelightDigital.aliases.embimedia.enabled=true adapters.limelightDigital.aliases.embimedia.endpoint=http://localhost:8090/embimedia-exchange/{{Host}}/{{PublisherID}} adapters.limelightDigital.aliases.filmzie.enabled=true adapters.limelightDigital.aliases.filmzie.endpoint=http://localhost:8090/filmzie-exchange/{{Host}}/{{PublisherID}} +adapters.limelightDigital.aliases.tgm.enabled=true +adapters.limelightDigital.aliases.tgm.endpoint=http://localhost:8090/tgm-exchange/{{Host}}/{{PublisherID}} adapters.lmkiviads.enabled=true adapters.lmkiviads.endpoint=http://localhost:8090/lm-kiviads-exchange/{{SourceId}}/{{Host}} adapters.lockerdome.enabled=true @@ -324,6 +332,8 @@ adapters.openx.enabled=true adapters.openx.endpoint=http://localhost:8090/openx-exchange adapters.operaads.enabled=true adapters.operaads.endpoint=http://localhost:8090/operaads-exchange +adapters.oraki.enabled=true +adapters.oraki.endpoint=http://localhost:8090/oraki-exchange adapters.orbidder.enabled=true adapters.orbidder.endpoint=http://localhost:8090/orbidder-exchange adapters.outbrain.enabled=true