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/extra/bundle/pom.xml b/extra/bundle/pom.xml
index bd14db698fc..ad9d306f578 100644
--- a/extra/bundle/pom.xml
+++ b/extra/bundle/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/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml
new file mode 100644
index 00000000000..abf009733b2
--- /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.12.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..816cf122214
--- /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.CorrectionsProvider;
+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
+ CorrectionsProvider correctionsProvider(List correctionsProducers) {
+ return new CorrectionsProvider(correctionsProducers);
+ }
+
+ @Bean
+ ResponseCorrectionModule responseCorrectionModule(CorrectionsProvider correctionsProvider) {
+ return new ResponseCorrectionModule(correctionsProvider, ObjectMapperProvider.mapper());
+ }
+}
diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java
new file mode 100644
index 00000000000..2afe514bf7d
--- /dev/null
+++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.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 CorrectionsProvider {
+
+ private final List correctionsProducers;
+
+ public CorrectionsProvider(List correctionsProducers) {
+ this.correctionsProducers = Objects.requireNonNull(correctionsProducers);
+ }
+
+ public List corrections(Config config, BidRequest bidRequest) {
+ return correctionsProducers.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..625c857a23e
--- /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,104 @@
+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.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.modules.pb.response.correction.core.CorrectionsProvider;
+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 CorrectionsProvider correctionsProvider;
+ private final ObjectMapper mapper;
+
+ public ResponseCorrectionAllProcessedBidResponsesHook(CorrectionsProvider correctionsProvider, ObjectMapper mapper) {
+ this.correctionsProvider = Objects.requireNonNull(correctionsProvider);
+ 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 = correctionsProvider.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..5ea3b583acd
--- /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.CorrectionsProvider;
+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 extends Hook, ? extends InvocationContext>> hooks;
+
+ public ResponseCorrectionModule(CorrectionsProvider correctionsProvider, ObjectMapper mapper) {
+ this.hooks = Collections.singleton(
+ new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, mapper));
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+
+ @Override
+ public Collection extends Hook, ? extends InvocationContext>> 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/CorrectionsProviderTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java
new file mode 100644
index 00000000000..c5e7ac2d3f0
--- /dev/null
+++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.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 CorrectionsProviderTest {
+
+ @Mock
+ private CorrectionProducer correctionProducer;
+
+ private CorrectionsProvider target;
+
+ @BeforeEach
+ public void setUp() {
+ target = new CorrectionsProvider(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..0171f17cc04
--- /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.CorrectionsProvider;
+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 CorrectionsProvider correctionsProvider;
+
+ 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(correctionsProvider, 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(correctionsProvider.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(correctionsProvider.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/pom.xml b/extra/modules/pom.xml
index 64def14267f..f9c7a337e5d 100644
--- a/extra/modules/pom.xml
+++ b/extra/modules/pom.xml
@@ -21,6 +21,7 @@
confiant-ad-quality
pb-richmedia-filter
fiftyone-devicedetection
+ pb-response-correction
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/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/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/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/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/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/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/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/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/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/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy
index fbbe08089fc..6b5b8f4adb0 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,7 +1,5 @@
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)
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy
new file mode 100644
index 00000000000..46af75deac6
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy
@@ -0,0 +1,13 @@
+package org.prebid.server.functional.model.config
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+
+@ToString(includeNames = true, ignoreNulls = true)
+@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
+class PbResponseCorrection {
+
+ Boolean enabled
+ AppVideoHtml appVideoHtml
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy
index 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/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy
index ef2575ea3ed..70c99a2a833 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy
@@ -10,6 +10,7 @@ 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 {
@@ -36,21 +37,21 @@ class Dependencies {
static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION)
.withNetwork(network)
- static final LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest"))
- .withNetwork(Dependencies.network)
- .withServices(LocalStackContainer.Service.S3)
+ static LocalStackContainer localStackContainer
static void start() {
if (IS_LAUNCH_CONTAINERS) {
- Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer])
- .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, localStackContainer].parallelStream()
- .forEach({ it.stop() })
+ .forEach({ it.stop() })
}
}
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/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/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
index 674a3fcf245..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;
@@ -1551,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)));
@@ -1569,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/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/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/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/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/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/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java
index b32be1580d2..3dd300524c0 100644
--- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java
+++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java
@@ -8,6 +8,7 @@
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;
@@ -30,6 +31,8 @@
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"},
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/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties
index dd767fd8937..eebc53e085f 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