From 5ca717cef89b43096aab4123fb6fd8ca5a82d0b3 Mon Sep 17 00:00:00 2001 From: Danylo Date: Thu, 11 Dec 2025 23:55:04 +0100 Subject: [PATCH] Refactor `MediaTypeProcessor` --- .../server/auction/ExchangeService.java | 99 ++++------- .../BidderRequestCleaner.java | 40 +++++ .../BidderRequestCurrencyBlocker.java | 54 ++++++ .../BidderRequestMediaFilter.java | 135 ++++++++++++++ .../BidderRequestPostProcessor.java | 14 ++ .../BidderRequestPreferredMediaProcessor.java | 159 +++++++++++++++++ .../BidderRequestRejectedException.java | 20 +++ .../CompositeBidderRequestPostProcessor.java | 44 +++++ .../BidderMediaTypeProcessor.java | 134 -------------- .../CompositeMediaTypeProcessor.java | 45 ----- .../MediaTypeProcessingResult.java | 25 --- .../MediaTypeProcessor.java | 10 -- .../MultiFormatMediaTypeProcessor.java | 143 --------------- .../org/prebid/server/bidder/BidderInfo.java | 6 +- .../spring/config/ServiceConfiguration.java | 44 +++-- .../server/auction/ExchangeServiceTest.java | 105 +++++------ .../BidderRequestCleanerTest.java | 70 ++++++++ .../BidderRequestCurrencyBlockerTest.java | 166 ++++++++++++++++++ .../BidderRequestMediaFilterTest.java} | 90 ++++++---- ...erRequestPreferredMediaProcessorTest.java} | 130 +++++++------- ...mpositeBidderRequestPostProcessorTest.java | 89 ++++++++++ .../CompositeMediaTypeProcessorTest.java | 96 ---------- 22 files changed, 1028 insertions(+), 690 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java create mode 100644 src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java delete mode 100644 src/main/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessor.java delete mode 100644 src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java delete mode 100644 src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java delete mode 100644 src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java delete mode 100644 src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java create mode 100644 src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleanerTest.java create mode 100644 src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlockerTest.java rename src/test/java/org/prebid/server/auction/{mediatypeprocessor/BidderMediaTypeProcessorTest.java => bidderrequestpostprocessor/BidderRequestMediaFilterTest.java} (61%) rename src/test/java/org/prebid/server/auction/{mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java => bidderrequestpostprocessor/BidderRequestPreferredMediaProcessorTest.java} (65%) create mode 100644 src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessorTest.java delete mode 100644 src/test/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessorTest.java diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 8bea0bc315e..c4ae087d205 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -19,7 +19,6 @@ import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -32,9 +31,9 @@ import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestPostProcessor; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestRejectedException; import org.prebid.server.auction.externalortb.StoredResponseProcessor; -import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; -import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidRejectionReason; @@ -51,7 +50,6 @@ import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.HttpBidderRequester; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.bidder.model.BidderBid; @@ -144,7 +142,7 @@ public class ExchangeService { private final ImpAdjuster impAdjuster; private final SupplyChainResolver supplyChainResolver; private final DebugResolver debugResolver; - private final MediaTypeProcessor mediaTypeProcessor; + private final BidderRequestPostProcessor bidderRequestPostProcessor; private final UidUpdater uidUpdater; private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; @@ -171,7 +169,7 @@ public ExchangeService(double logSamplingRate, ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, - MediaTypeProcessor mediaTypeProcessor, + BidderRequestPostProcessor bidderRequestPostProcessor, UidUpdater uidUpdater, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, @@ -198,7 +196,7 @@ public ExchangeService(double logSamplingRate, this.impAdjuster = Objects.requireNonNull(impAdjuster); this.supplyChainResolver = Objects.requireNonNull(supplyChainResolver); this.debugResolver = Objects.requireNonNull(debugResolver); - this.mediaTypeProcessor = Objects.requireNonNull(mediaTypeProcessor); + this.bidderRequestPostProcessor = Objects.requireNonNull(bidderRequestPostProcessor); this.uidUpdater = Objects.requireNonNull(uidUpdater); this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); @@ -435,7 +433,7 @@ private void removeInvalidBidRejectionTrackers(Map BidderAliases aliases) { final Set bidderNames = new HashSet<>(bidRejectionTrackers.keySet()); - for (String bidder: bidderNames) { + for (String bidder : bidderNames) { if (!isValidBidder(bidder, aliases)) { bidRejectionTrackers.remove(bidder); logger.warn("Pre-rejected impressions of the bidder {} have been removed. " @@ -492,13 +490,12 @@ private Future> extractAuctionParticipations( .filter(bidder -> isBidderCallActivityAllowed(bidder, context)) .distinct() .toList(); - final Map> impBidderToStoredBidResponse = - storedResponseResult.getImpBidderToStoredBidResponse(); + return makeAuctionParticipation( bidders, context, aliases, - impBidderToStoredBidResponse, + storedResponseResult.getImpBidderToStoredBidResponse(), imps, bidderToMultiBid); } @@ -539,7 +536,7 @@ private Future> makeAuctionParticipation( final BidRequest bidRequest = context.getBidRequest(); final ExtRequest requestExt = bidRequest.getExt(); - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; final Map biddersToConfigs = getBiddersToConfigs(prebid); final Map> eidPermissions = getEidPermissions(prebid); final Map> bidderToUserAndDevice = @@ -1157,69 +1154,39 @@ private Future processAndRequestBids(AuctionContext auctionConte Timeout timeout, BidderAliases aliases) { - final String bidderName = bidderRequest.getBidder(); - final MediaTypeProcessingResult mediaTypeProcessingResult = mediaTypeProcessor.process( - bidderRequest.getBidRequest(), bidderName, aliases, auctionContext.getAccount()); - final List mediaTypeProcessingErrors = mediaTypeProcessingResult.getErrors(); - if (mediaTypeProcessingResult.isRejected()) { - return processReject( - auctionContext, - BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, - mediaTypeProcessingErrors, - bidderName); - } - if (isUnacceptableCurrency(auctionContext, aliases.resolveBidder(bidderName))) { - return processReject( - auctionContext, - BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, - List.of(BidderError.generic("No match between the configured currencies and bidRequest.cur")), - bidderName); - } - - return Future.succeededFuture(mediaTypeProcessingResult.getBidRequest()) - .map(bidderRequest::with) - .compose(modifiedBidderRequest -> invokeHooksAndRequestBids( - auctionContext, modifiedBidderRequest, timeout, aliases)) - .map(bidderResponse -> bidderResponse.with( - addWarnings(bidderResponse.getSeatBid(), mediaTypeProcessingErrors))); - } - - private boolean isUnacceptableCurrency(AuctionContext auctionContext, String originalBidderName) { - final List requestCurrencies = auctionContext.getBidRequest().getCur(); - final List bidAcceptableCurrencies = - Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) - .map(BidderInfo::getCurrencyAccepted) - .orElse(null); - - if (CollectionUtils.isEmpty(requestCurrencies) || CollectionUtils.isEmpty(bidAcceptableCurrencies)) { - return false; - } - - return !CollectionUtils.containsAny(requestCurrencies, bidAcceptableCurrencies); - } - - private static Future processReject(AuctionContext auctionContext, - BidRejectionReason bidRejectionReason, - List warnings, - String bidderName) { - - auctionContext.getBidRejectionTrackers() - .get(bidderName) - .rejectAll(bidRejectionReason); - final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() - .warnings(warnings) - .build(); - return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + return bidderRequestPostProcessor.process(bidderRequest, aliases, auctionContext) + .compose(result -> invokeHooksAndRequestBids(auctionContext, result.getValue(), timeout, aliases) + .map(response -> response.with(addWarnings(response.getSeatBid(), result.getErrors())))) + .recover(throwable -> recoverBidderRequestRejection( + auctionContext, bidderRequest.getBidder(), throwable)); } private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { return CollectionUtils.isNotEmpty(warnings) ? seatBid.toBuilder() - .warnings(ListUtils.union(warnings, seatBid.getWarnings())) + .warnings(ListUtil.union(warnings, seatBid.getWarnings())) .build() : seatBid; } + private static Future recoverBidderRequestRejection(AuctionContext auctionContext, + String bidderName, + Throwable throwable) { + + if (throwable instanceof BidderRequestRejectedException rejection) { + auctionContext.getBidRejectionTrackers() + .get(bidderName) + .rejectAll(rejection.getRejectionReason()); + final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() + .warnings(rejection.getErrors()) + .build(); + + return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + } + + return Future.failedFuture(throwable); + } + private Future invokeHooksAndRequestBids(AuctionContext auctionContext, BidderRequest bidderRequest, Timeout timeout, diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java new file mode 100644 index 00000000000..9028d337014 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java @@ -0,0 +1,40 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Collections; + +public class BidderRequestCleaner implements BidderRequestPostProcessor { + + @Override + public Future> process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + final BidRequest bidRequest = bidderRequest.getBidRequest(); + final ExtRequest ext = bidRequest.getExt(); + final ExtRequestPrebid extPrebid = ext != null ? ext.getPrebid() : null; + final ObjectNode bidderControls = extPrebid != null ? extPrebid.getBiddercontrols() : null; + + if (bidderControls == null) { + return resultOf(bidderRequest); + } + + final ExtRequest cleanedExt = ExtRequest.of(extPrebid.toBuilder().biddercontrols(null).build()); + cleanedExt.addProperties(ext.getProperties()); + + return resultOf(bidderRequest.with(bidRequest.toBuilder().ext(cleanedExt).build())); + } + + private static Future> resultOf(BidderRequest bidderRequest) { + return Future.succeededFuture(Result.of(bidderRequest, Collections.emptyList())); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java new file mode 100644 index 00000000000..5cdeef5e8d3 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java @@ -0,0 +1,54 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class BidderRequestCurrencyBlocker implements BidderRequestPostProcessor { + + private final BidderCatalog bidderCatalog; + + public BidderRequestCurrencyBlocker(BidderCatalog bidderCatalog) { + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public Future> process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + if (isAcceptableCurrency(bidderRequest.getBidRequest(), aliases.resolveBidder(bidderRequest.getBidder()))) { + return Future.succeededFuture(Result.of(bidderRequest, Collections.emptyList())); + } + + return Future.failedFuture(new BidderRequestRejectedException( + BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, + List.of(BidderError.generic("No match between the configured currencies and bidRequest.cur")))); + } + + private boolean isAcceptableCurrency(BidRequest bidRequest, String originalBidderName) { + final List requestCurrencies = bidRequest.getCur(); + final Set bidAcceptableCurrencies = + Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) + .map(BidderInfo::getCurrencyAccepted) + .orElse(null); + + return CollectionUtils.isEmpty(requestCurrencies) + || CollectionUtils.isEmpty(bidAcceptableCurrencies) + || requestCurrencies.stream().anyMatch(bidAcceptableCurrencies::contains); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java new file mode 100644 index 00000000000..27166febb37 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java @@ -0,0 +1,135 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.spring.config.bidder.model.MediaType; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidderRequestMediaFilter implements BidderRequestPostProcessor { + + private static final EnumSet NONE_OF_MEDIA_TYPES = EnumSet.noneOf(MediaType.class); + + private final BidderCatalog bidderCatalog; + + public BidderRequestMediaFilter(BidderCatalog bidderCatalog) { + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public Future> process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + final String resolvedBidderName = aliases.resolveBidder(bidderRequest.getBidder()); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + final Set supportedMediaTypes = extractSupportedMediaTypes(bidRequest, resolvedBidderName); + if (supportedMediaTypes.isEmpty()) { + return rejected(List.of(BidderError.badInput("Bidder does not support any media types."))); + } + + final List errors = new ArrayList<>(); + final BidRequest modifiedBidRequest = processBidRequest(bidRequest, supportedMediaTypes, errors); + + return modifiedBidRequest != null + ? Future.succeededFuture(Result.of(bidderRequest.with(modifiedBidRequest), errors)) + : rejected(errors); + } + + private static Future> rejected(List errors) { + return Future.failedFuture( + new BidderRequestRejectedException(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, errors)); + } + + private Set extractSupportedMediaTypes(BidRequest bidRequest, String bidderName) { + final BidderInfo.CapabilitiesInfo capabilitiesInfo = bidderCatalog + .bidderInfoByName(bidderName) + .getCapabilities(); + + final BidderInfo.PlatformInfo site = bidRequest.getSite() != null ? capabilitiesInfo.getSite() : null; + final BidderInfo.PlatformInfo app = bidRequest.getApp() != null ? capabilitiesInfo.getApp() : null; + final BidderInfo.PlatformInfo dooh = bidRequest.getDooh() != null ? capabilitiesInfo.getDooh() : null; + + return Stream.of(site, app, dooh) + .filter(Objects::nonNull) + .findFirst() + .map(BidderInfo.PlatformInfo::getMediaTypes) + .filter(mediaTypes -> !mediaTypes.isEmpty()) + .map(EnumSet::copyOf) + .orElse(NONE_OF_MEDIA_TYPES); + } + + private static BidRequest processBidRequest(BidRequest bidRequest, + Set supportedMediaTypes, + List errors) { + + final List modifiedImps = bidRequest.getImp().stream() + .map(imp -> processImp(imp, supportedMediaTypes, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (modifiedImps.isEmpty()) { + errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); + return null; + } + + return bidRequest.toBuilder().imp(modifiedImps).build(); + } + + private static Imp processImp(Imp imp, Set supportedMediaTypes, List errors) { + final Set impMediaTypes = getMediaTypes(imp); + if (supportedMediaTypes.containsAll(impMediaTypes)) { + return imp; + } + + final Banner banner = supportedMediaTypes.contains(MediaType.BANNER) ? imp.getBanner() : null; + final Video video = supportedMediaTypes.contains(MediaType.VIDEO) ? imp.getVideo() : null; + final Audio audio = supportedMediaTypes.contains(MediaType.AUDIO) ? imp.getAudio() : null; + final Native xNative = supportedMediaTypes.contains(MediaType.NATIVE) ? imp.getXNative() : null; + + if (ObjectUtils.allNull(banner, video, audio, xNative)) { + errors.add(BidderError.badInput(""" + Imp %s does not have a supported media type \ + and has been removed from the request for this bidder.""".formatted(imp.getId()))); + + return null; + } + + return imp.toBuilder() + .banner(banner) + .video(video) + .audio(audio) + .xNative(xNative) + .build(); + } + + private static Set getMediaTypes(Imp imp) { + return Stream.of( + imp.getBanner() != null ? MediaType.BANNER : null, + imp.getVideo() != null ? MediaType.VIDEO : null, + imp.getAudio() != null ? MediaType.AUDIO : null, + imp.getXNative() != null ? MediaType.NATIVE : null) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(MediaType.class))); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java new file mode 100644 index 00000000000..f3acff4f5ff --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java @@ -0,0 +1,14 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.model.Result; + +public interface BidderRequestPostProcessor { + + Future> process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext); +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java new file mode 100644 index 00000000000..3ad073e78fb --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java @@ -0,0 +1,159 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.spring.config.bidder.model.MediaType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class BidderRequestPreferredMediaProcessor implements BidderRequestPostProcessor { + + private static final String PREF_MTYPE_FIELD = "prefmtype"; + + private final BidderCatalog bidderCatalog; + + public BidderRequestPreferredMediaProcessor(BidderCatalog bidderCatalog) { + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public Future> process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + final String bidderName = bidderRequest.getBidder(); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + + final String resolvedBidderName = aliases.resolveBidder(bidderName); + if (isMultiFormatSupported(resolvedBidderName)) { + return noAction(bidderRequest); + } + + final Optional preferredMediaType = preferredMediaType(bidRequest, bidderName) + .or(() -> preferredMediaType(auctionContext.getAccount(), resolvedBidderName)); + if (preferredMediaType.isEmpty()) { + return noAction(bidderRequest); + } + + final List errors = new ArrayList<>(); + final BidRequest modifiedBidRequest = processBidRequest(bidRequest, preferredMediaType.get(), errors); + + return modifiedBidRequest != null + ? Future.succeededFuture(Result.of(bidderRequest.with(modifiedBidRequest), errors)) + : Future.failedFuture(new BidderRequestRejectedException( + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, errors)); + } + + private static Future> noAction(BidderRequest bidderRequest) { + return Future.succeededFuture(Result.of(bidderRequest, Collections.emptyList())); + } + + private boolean isMultiFormatSupported(String bidder) { + return bidderCatalog.bidderInfoByName(bidder).getOrtb().isMultiFormatSupported(); + } + + private static Optional preferredMediaType(BidRequest bidRequest, String bidderName) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBiddercontrols) + .map(bidders -> getBidder(bidderName, bidders)) + .map(bidder -> bidder.get(PREF_MTYPE_FIELD)) + .filter(JsonNode::isTextual) + .map(JsonNode::textValue) + .map(MediaType::of); + } + + private static Optional preferredMediaType(Account account, String bidderName) { + return Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getPreferredMediaTypes) + .map(preferredMediaTypes -> preferredMediaTypes.get(bidderName)); + } + + private static JsonNode getBidder(String bidderName, JsonNode biddersNode) { + final Iterator fieldNames = biddersNode.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (StringUtils.equalsIgnoreCase(bidderName, fieldName)) { + return biddersNode.get(fieldName); + } + } + + return null; + } + + private static BidRequest processBidRequest(BidRequest bidRequest, + MediaType preferredMediaType, + List errors) { + + final List modifiedImps = bidRequest.getImp().stream() + .map(imp -> processImp(imp, preferredMediaType, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (modifiedImps.isEmpty()) { + errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); + return null; + } + + return bidRequest.toBuilder().imp(modifiedImps).build(); + } + + private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { + if (!isMultiFormat(imp)) { + return imp; + } + + final Banner banner = preferredMediaType == MediaType.BANNER ? imp.getBanner() : null; + final Video video = preferredMediaType == MediaType.VIDEO ? imp.getVideo() : null; + final Audio audio = preferredMediaType == MediaType.AUDIO ? imp.getAudio() : null; + final Native xNative = preferredMediaType == MediaType.NATIVE ? imp.getXNative() : null; + + if (ObjectUtils.allNull(banner, video, audio, xNative)) { + errors.add(BidderError.badInput(""" + Imp %s does not have a media type after filtering \ + and has been removed from the request for this bidder.""".formatted(imp.getId()))); + + return null; + } + + return imp.toBuilder() + .banner(banner) + .video(video) + .audio(audio) + .xNative(xNative) + .build(); + } + + private static boolean isMultiFormat(Imp imp) { + int count = 0; + return (imp.getBanner() != null && ++count > 1) + || (imp.getVideo() != null && ++count > 1) + || (imp.getAudio() != null && ++count > 1) + || (imp.getXNative() != null && ++count > 1); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java new file mode 100644 index 00000000000..6beeaa5069f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java @@ -0,0 +1,20 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import lombok.Getter; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.model.BidderError; + +import java.util.List; +import java.util.Objects; + +@Getter +public class BidderRequestRejectedException extends RuntimeException { + + private final BidRejectionReason rejectionReason; + private final List errors; + + public BidderRequestRejectedException(BidRejectionReason bidRejectionReason, List errors) { + this.rejectionReason = Objects.requireNonNull(bidRejectionReason); + this.errors = Objects.requireNonNull(errors); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java new file mode 100644 index 00000000000..033b4223927 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java @@ -0,0 +1,44 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class CompositeBidderRequestPostProcessor implements BidderRequestPostProcessor { + + private final List bidderRequestPostProcessors; + + public CompositeBidderRequestPostProcessor(List bidderRequestPostProcessors) { + this.bidderRequestPostProcessors = Objects.requireNonNull(bidderRequestPostProcessors); + } + + @Override + public Future> process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + Future> result = initialResult(bidderRequest); + for (BidderRequestPostProcessor bidderRequestPostProcessor : bidderRequestPostProcessors) { + result = result.compose(previous -> + bidderRequestPostProcessor.process(previous.getValue(), aliases, auctionContext) + .map(current -> mergeErrors(previous, current))); + } + + return result; + } + + private static Future> initialResult(BidderRequest bidderRequest) { + return Future.succeededFuture(Result.of(bidderRequest, Collections.emptyList())); + } + + private static Result mergeErrors(Result previous, Result current) { + return Result.of(current.getValue(), ListUtil.union(previous.getErrors(), current.getErrors())); + } +} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessor.java deleted file mode 100644 index 17828059175..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessor.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import org.apache.commons.collections4.SetUtils; -import org.prebid.server.auction.aliases.BidderAliases; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.bidder.BidderInfo; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.settings.model.Account; -import org.prebid.server.spring.config.bidder.model.MediaType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * {@link BidderMediaTypeProcessor} is an implementation of {@link MediaTypeProcessor} that - * can be used to remove media types from {@link Imp} unsupported by specific bidder. - */ -public class BidderMediaTypeProcessor implements MediaTypeProcessor { - - private static final EnumSet NONE_OF_MEDIA_TYPES = EnumSet.noneOf(MediaType.class); - - private final BidderCatalog bidderCatalog; - - public BidderMediaTypeProcessor(BidderCatalog bidderCatalog) { - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - } - - @Override - public MediaTypeProcessingResult process(BidRequest bidRequest, - String bidderName, - BidderAliases aliases, - Account account) { - final String resolvedBidderName = aliases.resolveBidder(bidderName); - final Set supportedMediaTypes = extractSupportedMediaTypes(bidRequest, resolvedBidderName); - if (supportedMediaTypes.isEmpty()) { - return MediaTypeProcessingResult.rejected(Collections.singletonList( - BidderError.badInput("Bidder does not support any media types."))); - } - - final List errors = new ArrayList<>(); - final BidRequest modifiedBidRequest = processBidRequest(bidRequest, supportedMediaTypes, errors); - - return modifiedBidRequest != null - ? MediaTypeProcessingResult.succeeded(modifiedBidRequest, errors) - : MediaTypeProcessingResult.rejected(errors); - } - - private Set extractSupportedMediaTypes(BidRequest bidRequest, String bidderName) { - final BidderInfo.CapabilitiesInfo capabilitiesInfo = bidderCatalog.bidderInfoByName(bidderName) - .getCapabilities(); - - final Supplier fetchSupportedMediaTypes; - if (bidRequest.getSite() != null) { - fetchSupportedMediaTypes = capabilitiesInfo::getSite; - } else if (bidRequest.getApp() != null) { - fetchSupportedMediaTypes = capabilitiesInfo::getApp; - } else { - fetchSupportedMediaTypes = capabilitiesInfo::getDooh; - } - - return Optional.ofNullable(fetchSupportedMediaTypes.get()) - .map(BidderInfo.PlatformInfo::getMediaTypes) - .filter(mediaTypes -> !mediaTypes.isEmpty()) - .map(EnumSet::copyOf) - .orElse(NONE_OF_MEDIA_TYPES); - } - - private BidRequest processBidRequest(BidRequest bidRequest, - Set supportedMediaTypes, - List errors) { - - final List modifiedImps = bidRequest.getImp().stream() - .map(imp -> processImp(imp, supportedMediaTypes, errors)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - if (modifiedImps.isEmpty()) { - errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); - return null; - } - - return bidRequest.toBuilder().imp(modifiedImps).build(); - } - - private static Imp processImp(Imp imp, Set supportedMediaTypes, List errors) { - final Set impMediaTypes = getMediaTypes(imp); - final Set unsupportedMediaTypes = SetUtils.difference(impMediaTypes, supportedMediaTypes); - - if (unsupportedMediaTypes.isEmpty()) { - return imp; - } - - if (impMediaTypes.equals(unsupportedMediaTypes)) { - errors.add(BidderError.badInput("Imp " + imp.getId() + " does not have a supported media type " - + "and has been removed from the request for this bidder.")); - - return null; - } - - final Imp.ImpBuilder impBuilder = imp.toBuilder(); - unsupportedMediaTypes.forEach(unsupportedMediaType -> removeMediaType(impBuilder, unsupportedMediaType)); - - return impBuilder.build(); - } - - private static Set getMediaTypes(Imp imp) { - return Stream.of( - imp.getBanner() != null ? MediaType.BANNER : null, - imp.getVideo() != null ? MediaType.VIDEO : null, - imp.getAudio() != null ? MediaType.AUDIO : null, - imp.getXNative() != null ? MediaType.NATIVE : null) - .filter(Objects::nonNull) - .collect(Collectors.toCollection(() -> EnumSet.noneOf(MediaType.class))); - } - - private static void removeMediaType(Imp.ImpBuilder impBuilder, MediaType mediaType) { - switch (mediaType) { - case BANNER -> impBuilder.banner(null); - case VIDEO -> impBuilder.video(null); - case AUDIO -> impBuilder.audio(null); - case NATIVE -> impBuilder.xNative(null); - } - } -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java deleted file mode 100644 index 5dbd2bb3ae6..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import org.prebid.server.auction.aliases.BidderAliases; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.settings.model.Account; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class CompositeMediaTypeProcessor implements MediaTypeProcessor { - - private final List mediaTypeProcessors; - - public CompositeMediaTypeProcessor(List mediaTypeProcessors) { - this.mediaTypeProcessors = Objects.requireNonNull(mediaTypeProcessors); - } - - @Override - public MediaTypeProcessingResult process(BidRequest originalBidRequest, - String bidderName, - BidderAliases aliases, - Account account) { - BidRequest bidRequest = originalBidRequest; - final List errors = new ArrayList<>(); - - for (MediaTypeProcessor mediaTypeProcessor : mediaTypeProcessors) { - final MediaTypeProcessingResult result = mediaTypeProcessor.process( - bidRequest, - bidderName, - aliases, - account); - - bidRequest = result.getBidRequest(); - errors.addAll(result.getErrors()); - - if (result.isRejected()) { - return MediaTypeProcessingResult.rejected(errors); - } - } - - return MediaTypeProcessingResult.succeeded(bidRequest, errors); - } -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java deleted file mode 100644 index b9a6780d2e8..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import lombok.Value; -import org.prebid.server.bidder.model.BidderError; - -import java.util.List; - -@Value(staticConstructor = "of") -public class MediaTypeProcessingResult { - - BidRequest bidRequest; - - List errors; - - boolean rejected; - - public static MediaTypeProcessingResult succeeded(BidRequest bidRequest, List errors) { - return MediaTypeProcessingResult.of(bidRequest, errors, false); - } - - public static MediaTypeProcessingResult rejected(List errors) { - return MediaTypeProcessingResult.of(null, errors, true); - } -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java deleted file mode 100644 index 0cf5f19bd6a..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import org.prebid.server.auction.aliases.BidderAliases; -import org.prebid.server.settings.model.Account; - -public interface MediaTypeProcessor { - - MediaTypeProcessingResult process(BidRequest bidRequest, String bidderName, BidderAliases aliases, Account account); -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java deleted file mode 100644 index 0fda09f919e..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.fasterxml.jackson.databind.JsonNode; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.aliases.BidderAliases; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAuctionConfig; -import org.prebid.server.spring.config.bidder.model.MediaType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class MultiFormatMediaTypeProcessor implements MediaTypeProcessor { - - private static final String PREF_MTYPE_FIELD = "prefmtype"; - - private final BidderCatalog bidderCatalog; - - public MultiFormatMediaTypeProcessor(BidderCatalog bidderCatalog) { - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - } - - @Override - public MediaTypeProcessingResult process(BidRequest bidRequest, - String bidderName, - BidderAliases aliases, - Account account) { - final String resolvedBidderName = aliases.resolveBidder(bidderName); - //todo: ext.prebid.biddercontrols clean-up should NOT be here - // Suggestion: keep biddercontrols in the Auction Context - // and clean it up on the extraction auction participants step - final BidRequest.BidRequestBuilder bidRequestBuilder = Optional.ofNullable(bidRequest.getExt()) - .map(ExtRequest::getPrebid) - .map(prebid -> prebid.toBuilder().biddercontrols(null).build()) - .map(prebid -> { - final ExtRequest extRequest = ExtRequest.of(prebid); - extRequest.addProperties(bidRequest.getExt().getProperties()); - return extRequest; - }) - .map(extRequest -> bidRequest.toBuilder().ext(extRequest)) - .orElse(bidRequest.toBuilder()); - if (isMultiFormatSupported(resolvedBidderName)) { - return MediaTypeProcessingResult.succeeded(bidRequestBuilder.build(), Collections.emptyList()); - } - - final MediaType preferredMediaType = preferredMediaType(bidRequest, account, bidderName, resolvedBidderName); - if (preferredMediaType == null) { - return MediaTypeProcessingResult.succeeded(bidRequestBuilder.build(), Collections.emptyList()); - } - - final List errors = new ArrayList<>(); - final List updatedImps = bidRequest.getImp().stream() - .map(imp -> processImp(imp, preferredMediaType, errors)) - .filter(Objects::nonNull) - .toList(); - - if (updatedImps.isEmpty()) { - errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); - return MediaTypeProcessingResult.rejected(errors); - } - - return MediaTypeProcessingResult.succeeded(bidRequestBuilder.imp(updatedImps).build(), errors); - } - - private boolean isMultiFormatSupported(String bidder) { - return bidderCatalog.bidderInfoByName(bidder).getOrtb().isMultiFormatSupported(); - } - - private MediaType preferredMediaType(BidRequest bidRequest, - Account account, - String originalBidderName, - String resolvedBidderName) { - - return Optional.ofNullable(bidRequest.getExt()) - .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getBiddercontrols) - .map(bidders -> getBidder(originalBidderName, bidders)) - .map(bidder -> bidder.get(PREF_MTYPE_FIELD)) - .filter(JsonNode::isTextual) - .map(JsonNode::textValue) - .map(MediaType::of) - .or(() -> Optional.ofNullable(account.getAuction()) - .map(AccountAuctionConfig::getPreferredMediaTypes) - .map(preferredMediaTypes -> preferredMediaTypes.get(resolvedBidderName))) - .orElse(null); - } - - private static JsonNode getBidder(String bidderName, JsonNode biddersNode) { - final Iterator fieldNames = biddersNode.fieldNames(); - while (fieldNames.hasNext()) { - final String fieldName = fieldNames.next(); - if (StringUtils.equalsIgnoreCase(bidderName, fieldName)) { - return biddersNode.get(fieldName); - } - } - return null; - } - - private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { - if (!isMultiFormat(imp)) { - return imp; - } - - final Imp updatedImp = switch (preferredMediaType) { - case BANNER -> imp.getBanner() != null - ? imp.toBuilder().video(null).audio(null).xNative(null).build() - : null; - case VIDEO -> imp.getVideo() != null - ? imp.toBuilder().banner(null).audio(null).xNative(null).build() - : null; - case AUDIO -> imp.getAudio() != null - ? imp.toBuilder().banner(null).video(null).xNative(null).build() - : null; - case NATIVE -> imp.getXNative() != null - ? imp.toBuilder().banner(null).video(null).audio(null).build() - : null; - }; - - if (updatedImp == null) { - errors.add(BidderError.badInput("Imp " + imp.getId() + " does not have a media type after filtering " - + "and has been removed from the request for this bidder.")); - } - return updatedImp; - } - - private static boolean isMultiFormat(Imp imp) { - int count = 0; - return (imp.getBanner() != null && ++count > 1) - || (imp.getVideo() != null && ++count > 1) - || (imp.getAudio() != null && ++count > 1) - || (imp.getXNative() != null && ++count > 1); - } -} diff --git a/src/main/java/org/prebid/server/bidder/BidderInfo.java b/src/main/java/org/prebid/server/bidder/BidderInfo.java index 1ff8f323701..d26cc2c4b2c 100644 --- a/src/main/java/org/prebid/server/bidder/BidderInfo.java +++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java @@ -7,7 +7,9 @@ import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.MediaType; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Value(staticConstructor = "of") public class BidderInfo { @@ -28,7 +30,7 @@ public class BidderInfo { List vendors; - List currencyAccepted; + Set currencyAccepted; GdprInfo gdpr; @@ -72,7 +74,7 @@ public static BidderInfo create(boolean enabled, platformInfo(siteMediaTypes), platformInfo(doohMediaTypes)), supportedVendors, - currencyAccepted, + currencyAccepted != null ? new HashSet<>(currencyAccepted) : null, new GdprInfo(vendorId), ccpaEnforced, modifyingVastXmlAllowed, diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 228d4702435..2bb6e853bcd 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -33,6 +33,12 @@ import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.WinningBidComparatorFactory; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestCleaner; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestCurrencyBlocker; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestMediaFilter; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestPostProcessor; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestPreferredMediaProcessor; +import org.prebid.server.auction.bidderrequestpostprocessor.CompositeBidderRequestPostProcessor; import org.prebid.server.auction.categorymapping.BasicCategoryMappingService; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.categorymapping.NoOpCategoryMappingService; @@ -47,10 +53,6 @@ import org.prebid.server.auction.gpp.processor.GppContextProcessor; import org.prebid.server.auction.gpp.processor.tcfeuv2.TcfEuV2ContextProcessor; import org.prebid.server.auction.gpp.processor.uspv1.UspV1ContextProcessor; -import org.prebid.server.auction.mediatypeprocessor.BidderMediaTypeProcessor; -import org.prebid.server.auction.mediatypeprocessor.CompositeMediaTypeProcessor; -import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; -import org.prebid.server.auction.mediatypeprocessor.MultiFormatMediaTypeProcessor; import org.prebid.server.auction.privacy.contextfactory.AmpPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.CookieSyncPrivacyContextFactory; @@ -136,6 +138,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import jakarta.validation.constraints.Min; import java.io.IOException; @@ -792,19 +796,35 @@ BidderCatalog bidderCatalog(List bidderDeps) { } @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + BidderRequestPostProcessor bidderRequestCurrencyBlocker(BidderCatalog bidderCatalog) { + return new BidderRequestCurrencyBlocker(bidderCatalog); + } + + @Bean + @Order(0) @ConditionalOnProperty(prefix = "auction.filter-imp-media-type", name = "enabled", havingValue = "true") - MediaTypeProcessor bidderMediaTypeProcessor(BidderCatalog bidderCatalog) { - return new BidderMediaTypeProcessor(bidderCatalog); + BidderRequestPostProcessor bidderRequestMediaFilter(BidderCatalog bidderCatalog) { + return new BidderRequestMediaFilter(bidderCatalog); } @Bean - MediaTypeProcessor multiFormatMediaTypeProcessor(BidderCatalog bidderCatalog) { - return new MultiFormatMediaTypeProcessor(bidderCatalog); + @Order(0) + BidderRequestPostProcessor bidderRequestPreferredMediaProcessor(BidderCatalog bidderCatalog) { + return new BidderRequestPreferredMediaProcessor(bidderCatalog); } @Bean - CompositeMediaTypeProcessor compositeMediaTypeProcessor(List mediaTypeProcessors) { - return new CompositeMediaTypeProcessor(mediaTypeProcessors); + @Order(Ordered.LOWEST_PRECEDENCE) + BidderRequestPostProcessor bidderRequestCleaner() { + return new BidderRequestCleaner(); + } + + @Bean + CompositeBidderRequestPostProcessor compositeBidderRequestPostProcessor( + List bidderRequestPostProcessors) { + + return new CompositeBidderRequestPostProcessor(bidderRequestPostProcessors); } @Bean @@ -916,7 +936,7 @@ ExchangeService exchangeService( ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, - CompositeMediaTypeProcessor mediaTypeProcessor, + CompositeBidderRequestPostProcessor bidderRequestPostProcessor, UidUpdater uidUpdater, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, @@ -944,7 +964,7 @@ ExchangeService exchangeService( impAdjuster, supplyChainResolver, debugResolver, - mediaTypeProcessor, + bidderRequestPostProcessor, uidUpdater, timeoutResolver, timeoutFactory, diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 20561c5b685..582b5341f24 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -43,18 +43,20 @@ import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestPostProcessor; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestRejectedException; import org.prebid.server.auction.externalortb.StoredResponseProcessor; -import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; -import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionReason; import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.auction.model.MultiBidConfig; +import org.prebid.server.auction.model.Rejection; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; @@ -69,6 +71,7 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; @@ -208,7 +211,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.prebid.server.auction.model.BidRejectionReason.NO_BID; -import static org.prebid.server.auction.model.BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY; +import static org.prebid.server.auction.model.BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -237,7 +240,7 @@ public class ExchangeServiceTest extends VertxTest { private DebugResolver debugResolver; @Mock(strictness = LENIENT) - private MediaTypeProcessor mediaTypeProcessor; + private BidderRequestPostProcessor bidderRequestPostProcessor; @Mock(strictness = LENIENT) private UidUpdater uidUpdater; @@ -363,8 +366,8 @@ public void setUp() { given(bidsAdjuster.validateAndAdjustBids(any(), any(), any())) .willAnswer(invocation -> invocation.getArgument(0)); - given(mediaTypeProcessor.process(any(), anyString(), any(), any())) - .willAnswer(invocation -> MediaTypeProcessingResult.succeeded(invocation.getArgument(0), emptyList())); + given(bidderRequestPostProcessor.process(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(Result.of(invocation.getArgument(0), emptyList()))); given(uidUpdater.updateUid(any(), any(), any())) .willAnswer(inv -> Optional.ofNullable((AuctionContext) inv.getArgument(1)) @@ -3923,7 +3926,7 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb Deal.builder().id("dealId2").build())) .build())); final BidRequest bidRequest = givenBidRequest(singletonList(imp), identity()); - final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder().build(); + final AuctionContext auctionContext = givenRequestContext(bidRequest); final BidderBid bidderBid = givenBidderBid( Bid.builder().id("bidId2").impid("impId1").dealid("dealId2").price(BigDecimal.ONE).build()); @@ -3958,7 +3961,7 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb Deal.builder().id("dealId2").build())) .build())); final BidRequest bidRequest = givenBidRequest(singletonList(imp), identity()); - final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder().build(); + final AuctionContext auctionContext = givenRequestContext(bidRequest); givenBidder(givenSeatBid(emptyList())); @@ -3976,75 +3979,52 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb } @Test - public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() { + public void shouldResponseWithAddedWarningsFromBidderRequestPostProcessor() { // given final Imp imp = givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")); final BidRequest bidRequest = givenBidRequest(singletonList(imp), identity()); - final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder().build(); + final AuctionContext auctionContext = givenRequestContext(bidRequest); - given(mediaTypeProcessor.process(any(), anyString(), any(), any())) - .willReturn(MediaTypeProcessingResult.rejected(Collections.singletonList( - BidderError.badInput("MediaTypeProcessor error.")))); - given(bidResponseCreator.create( - argThat(argument -> argument.getAuctionParticipations().getFirst() - .getBidderResponse() - .equals(BidderResponse.of( - "bidder1", - BidderSeatBid.builder() - .warnings(Collections.singletonList( - BidderError.badInput("MediaTypeProcessor error."))) - .build(), - 0))), - any(), - any())) - .willReturn(Future.succeededFuture(BidResponse.builder().id("uniqId").build())); + given(bidderRequestPostProcessor.process(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(Result.of( + invocation.getArgument(0), + singletonList(BidderError.badInput("BidderRequestPostProcessor error."))))); + givenBidder(givenSeatBid(emptyList())); // when - final Future result = target.holdAuction(auctionContext); + target.holdAuction(auctionContext); // then - assertThat(result.result()) - .extracting(AuctionContext::getBidResponse) - .isEqualTo(BidResponse.builder().id("uniqId").build()); + verify(httpBidderRequester).requestBids(any(), any(), any(), any(), any(), any(), anyBoolean()); + + final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(storedResponseProcessor).updateStoredBidResponse(captor.capture()); + assertThat(captor.getValue()) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getWarnings) + .containsExactly(BidderError.badInput("BidderRequestPostProcessor error.")); } @Test - public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() { + public void shouldResponseWithEmptySeatBidIfRejectedByBidderRequestPostProcessor() { // given final Imp imp = givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")); - final BidRequest bidRequest = givenBidRequest(singletonList(imp), - bidRequestBuilder -> bidRequestBuilder.cur(singletonList("USD"))); + final BidRequest bidRequest = givenBidRequest(singletonList(imp), identity()); final AuctionContext auctionContext = givenRequestContext(bidRequest); - given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create( - true, - null, - false, - null, - null, - null, - null, - null, - null, - null, - 0, - singletonList("CAD"), - false, - false, - CompressionType.NONE, - Ortb.of(false), - 0L)); - + given(bidderRequestPostProcessor.process(any(), any(), any())) + .willReturn(Future.failedFuture(new BidderRequestRejectedException( + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, + singletonList(BidderError.badInput("BidderRequestPostProcessor error."))))); given(bidResponseCreator.create( argThat(argument -> argument.getAuctionParticipations().getFirst() .getBidderResponse() .equals(BidderResponse.of( "bidder1", BidderSeatBid.builder() - .warnings(Collections.singletonList( - BidderError.generic( - "No match between the configured currencies and bidRequest.cur" - ))) + .warnings(singletonList( + BidderError.badInput("BidderRequestPostProcessor error."))) .build(), 0))), any(), @@ -4055,14 +4035,17 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() { final Future result = target.holdAuction(auctionContext); // then + verifyNoInteractions(httpBidderRequester); assertThat(result.result()) .extracting(AuctionContext::getBidResponse) .isEqualTo(BidResponse.builder().id("uniqId").build()); assertThat(result.result()) .extracting(AuctionContext::getBidRejectionTrackers) - .extracting(rejectionTrackers -> rejectionTrackers.get("bidder1")) - .extracting(BidRejectionTracker::getRejected) - .isEqualTo(Set.of(ImpRejection.of("bidder1", "impId1", REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY))); + .extracting(trackers -> trackers.get("bidder1")) + .extracting(BidRejectionTracker::getAllRejected) + .extracting(rejections -> rejections.get("impId1")) + .asInstanceOf(InstanceOfAssertFactories.list(Rejection.class)) + .containsExactly(ImpRejection.of("bidder1", "impId1", REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE)); } @Test @@ -4249,7 +4232,7 @@ private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { fpdResolver, impAdjuster, supplyChainResolver, debugResolver, - mediaTypeProcessor, + bidderRequestPostProcessor, uidUpdater, timeoutResolver, timeoutFactory, diff --git a/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleanerTest.java b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleanerTest.java new file mode 100644 index 00000000000..dec73e28f79 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleanerTest.java @@ -0,0 +1,70 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +public class BidderRequestCleanerTest extends VertxTest { + + private static final String BIDDER = "bidder"; + + private BidderRequestCleaner target; + + @BeforeEach + public void setUp() { + target = new BidderRequestCleaner(); + } + + @Test + public void processShouldReturnSameRequest() { + // given + final BidderRequest bidderRequest = givenBidderRequest(null); + + // when + final Result result = target.process(bidderRequest, null, null).result(); + + // then + assertThat(result.getValue()).isSameAs(bidderRequest); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void processShouldReturnCleanedRequest() { + // given + final BidderRequest bidderRequest = givenBidderRequest(mapper.createObjectNode()); + + // when + final Result result = target.process(bidderRequest, null, null).result(); + + // then + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBiddercontrols) + .isNull(); + assertThat(result.getErrors()).isEmpty(); + } + + private static BidderRequest givenBidderRequest(ObjectNode bidderControls) { + return BidderRequest.builder() + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .biddercontrols(bidderControls) + .build())) + .build()) + .bidder(BIDDER) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlockerTest.java b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlockerTest.java new file mode 100644 index 00000000000..b85616a116a --- /dev/null +++ b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlockerTest.java @@ -0,0 +1,166 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.assertj.core.api.InstanceOfAssertFactories; +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.aliases.BidderAliases; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.versionconverter.OrtbVersion; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.spring.config.bidder.model.CompressionType; +import org.prebid.server.spring.config.bidder.model.Ortb; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class BidderRequestCurrencyBlockerTest { + + private static final String BIDDER = "bidder"; + + @Mock + private BidderCatalog bidderCatalog; + @Mock + private BidderAliases bidderAliases; + + private BidderRequestCurrencyBlocker target; + + @BeforeEach + public void setUp() { + target = new BidderRequestCurrencyBlocker(bidderCatalog); + when(bidderAliases.resolveBidder(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + public void processShouldReturnSameRequestIfAcceptedCurrenciesAreNotConfigured() { + // given + given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(null)); + final BidderRequest bidderRequest = givenBidderRequest(List.of("USD")); + + // when + final Result result = target.process(bidderRequest, bidderAliases, null).result(); + + // then + assertThat(result.getValue()).isSameAs(bidderRequest); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void processShouldReturnSameRequestIfAcceptedCurrenciesAreEmpty() { + // given + given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(emptyList())); + final BidderRequest bidderRequest = givenBidderRequest(List.of("USD")); + + // when + final Result result = target.process(bidderRequest, bidderAliases, null).result(); + + // then + assertThat(result.getValue()).isSameAs(bidderRequest); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void processShouldReturnSameRequestIfRequestCurrencyIsNull() { + // given + given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(List.of("USD"))); + final BidderRequest bidderRequest = givenBidderRequest(null); + + // when + final Result result = target.process(bidderRequest, bidderAliases, null).result(); + + // then + assertThat(result.getValue()).isSameAs(bidderRequest); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void processShouldReturnSameRequestIfRequestCurrencyIsEmpty() { + // given + given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(List.of("USD"))); + final BidderRequest bidderRequest = givenBidderRequest(emptyList()); + + // when + final Result result = target.process(bidderRequest, bidderAliases, null).result(); + + // then + assertThat(result.getValue()).isSameAs(bidderRequest); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void processShouldReturnSameRequestIfRequestCurrencyContainsAcceptedCurrency() { + // given + given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(List.of("EUR", "USD"))); + final BidderRequest bidderRequest = givenBidderRequest(List.of("UAH", "USD")); + + // when + final Result result = target.process(bidderRequest, bidderAliases, null).result(); + + // then + assertThat(result.getValue()).isSameAs(bidderRequest); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void processShouldReturnFailureIfRequestDoesNotContainsAcceptedCurrency() { + // given + given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(List.of("EUR", "USD"))); + final BidderRequest bidderRequest = givenBidderRequest(List.of("UAH")); + + // when + final Future> result = target.process(bidderRequest, bidderAliases, null); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause()) + .asInstanceOf(InstanceOfAssertFactories.type(BidderRequestRejectedException.class)) + .satisfies(e -> { + assertThat(e.getRejectionReason()) + .isEqualTo(BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY); + assertThat(e.getErrors()).containsExactly( + BidderError.generic("No match between the configured currencies and bidRequest.cur")); + }); + } + + private static BidderInfo givenBidderInfo(List currencies) { + return BidderInfo.create( + true, + OrtbVersion.ORTB_2_6, + false, + "endpoint", + null, + "maintainerEmail", + null, + null, + null, + emptyList(), + 0, + currencies, + false, + false, + CompressionType.NONE, + Ortb.of(false), + 0L); + } + + private static BidderRequest givenBidderRequest(List currencies) { + return BidderRequest.builder() + .bidRequest(BidRequest.builder().cur(currencies).build()) + .bidder(BIDDER) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilterTest.java similarity index 61% rename from src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java rename to src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilterTest.java index d6e002489f8..00133131e29 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilterTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.mediatypeprocessor; +package org.prebid.server.auction.bidderrequestpostprocessor; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Audio; @@ -8,6 +8,8 @@ import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,10 +17,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.MediaType; import org.prebid.server.spring.config.bidder.model.Ortb; @@ -41,7 +46,7 @@ import static org.prebid.server.spring.config.bidder.model.MediaType.VIDEO; @ExtendWith(MockitoExtension.class) -public class BidderMediaTypeProcessorTest extends VertxTest { +public class BidderRequestMediaFilterTest extends VertxTest { private static final String BIDDER = "bidder"; @@ -50,42 +55,50 @@ public class BidderMediaTypeProcessorTest extends VertxTest { @Mock private BidderAliases bidderAliases; - private BidderMediaTypeProcessor target; + private BidderRequestMediaFilter target; @BeforeEach public void setUp() { - target = new BidderMediaTypeProcessor(bidderCatalog); + target = new BidderRequestMediaFilter(bidderCatalog); when(bidderAliases.resolveBidder(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); } @Test - public void processShouldReturnRejectedResultAndErrorIfBidderNotSupportAnyMediaType() { + public void processShouldReturnRejectedResultAndErrorIfBidderDoesNotSupportAnyMediaType() { // given given(bidderCatalog.bidderInfoByName(BIDDER)) .willReturn(givenBidderInfo(emptyList(), emptyList(), emptyList())); - final BidRequest bidRequest = givenBidRequest(identity(), givenImp(BANNER)); + final BidderRequest bidderRequest = givenBidderRequest(identity(), givenImp(BANNER)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, null); + final Future> result = target.process(bidderRequest, bidderAliases, null); // then - assertThat(result.isRejected()).isTrue(); - assertThat(result.getErrors()).containsExactly( - BidderError.badInput("Bidder does not support any media types.")); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()) + .asInstanceOf(InstanceOfAssertFactories.type(BidderRequestRejectedException.class)) + .satisfies(e -> { + assertThat(e.getRejectionReason()) + .isEqualTo(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE); + assertThat(e.getErrors()).containsExactly( + BidderError.badInput("Bidder does not support any media types.")); + }); } @Test public void processShouldUseAppMediaTypesIfAppPresent() { // given - final BidRequest bidRequest = givenBidRequest(request -> request.app(App.builder().build()), givenImp(BANNER)); given(bidderCatalog.bidderInfoByName(BIDDER)) .willReturn(givenBidderInfo(singletonList(BANNER), singletonList(AUDIO), singletonList(NATIVE))); + final BidderRequest bidderRequest = givenBidderRequest( + request -> request.app(App.builder().build()), + givenImp(BANNER)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, null); + final Result result = target.process(bidderRequest, bidderAliases, null).result(); // then - assertThat(result.getBidRequest()).isEqualTo(bidRequest); + assertThat(result.getValue()).isEqualTo(bidderRequest); assertThat(result.getErrors()).isEmpty(); } @@ -94,18 +107,19 @@ public void processShouldRemoveUnsupportedMediaTypes() { // given given(bidderCatalog.bidderInfoByName(BIDDER)) .willReturn(givenBidderInfo(emptyList(), List.of(BANNER, AUDIO), emptyList())); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( request -> request.site(Site.builder().build()), givenImp(AUDIO, NATIVE), givenImp(BANNER, VIDEO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, null); + final Result result = target.process(bidderRequest, bidderAliases, null).result(); // then - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(givenImp(AUDIO), givenImp(BANNER)); assertThat(result.getErrors()).isEmpty(); } @@ -115,18 +129,19 @@ public void processShouldRemoveImpWithOnlyUnsupportedMediaTypes() { // given given(bidderCatalog.bidderInfoByName(BIDDER)) .willReturn(givenBidderInfo(emptyList(), asList(BANNER, AUDIO), emptyList())); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( request -> request.site(Site.builder().build()), givenImp(VIDEO, NATIVE), givenImp(BANNER, AUDIO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, null); + final Result result = target.process(bidderRequest, bidderAliases, null).result(); // then - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(givenImp(BANNER, AUDIO)); assertThat(result.getErrors()).containsExactly( BidderError.badInput("Imp " + null + " does not have a supported media type " @@ -138,22 +153,28 @@ public void processShouldReturnRejectedResultIfRequestDoesNotContainsAnyImpWithS // given given(bidderCatalog.bidderInfoByName(BIDDER)) .willReturn(givenBidderInfo(emptyList(), asList(BANNER, AUDIO), emptyList())); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( request -> request.site(Site.builder().build()), givenImp(VIDEO), givenImp(NATIVE)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, null); + final Future> result = target.process(bidderRequest, bidderAliases, null); // then - assertThat(result.isRejected()).isTrue(); - assertThat(result.getErrors()).containsExactly( - BidderError.badInput("Imp " + null + " does not have a supported media type " - + "and has been removed from the request for this bidder."), - BidderError.badInput("Imp " + null + " does not have a supported media type " - + "and has been removed from the request for this bidder."), - BidderError.badInput("Bid request contains 0 impressions after filtering.")); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()) + .asInstanceOf(InstanceOfAssertFactories.type(BidderRequestRejectedException.class)) + .satisfies(e -> { + assertThat(e.getRejectionReason()) + .isEqualTo(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE); + assertThat(e.getErrors()).containsExactly( + BidderError.badInput("Imp null does not have a supported media type" + + " and has been removed from the request for this bidder."), + BidderError.badInput("Imp null does not have a supported media type" + + " and has been removed from the request for this bidder."), + BidderError.badInput("Bid request contains 0 impressions after filtering.")); + }); } private static BidderInfo givenBidderInfo(List appMediaTypes, @@ -179,10 +200,13 @@ private static BidderInfo givenBidderInfo(List appMediaTypes, 0L); } - private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, - Imp... imps) { + private static BidderRequest givenBidderRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { - return bidRequestCustomizer.apply(BidRequest.builder()).imp(asList(imps)).build(); + return BidderRequest.builder() + .bidRequest(bidRequestCustomizer.apply(BidRequest.builder()).imp(asList(imps)).build()) + .bidder(BIDDER) + .build(); } private static Imp givenImp(MediaType... mediaTypes) { diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessorTest.java similarity index 65% rename from src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java rename to src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessorTest.java index a8da73fe320..99050665bcc 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessorTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.mediatypeprocessor; +package org.prebid.server.auction.bidderrequestpostprocessor; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Audio; @@ -7,6 +7,7 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Video; +import io.vertx.core.Future; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,10 +16,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.settings.model.Account; @@ -44,7 +49,7 @@ import static org.prebid.server.spring.config.bidder.model.MediaType.VIDEO; @ExtendWith(MockitoExtension.class) -public class MultiFormatMediaTypeProcessorTest extends VertxTest { +public class BidderRequestPreferredMediaProcessorTest extends VertxTest { private static final String BIDDER = "bidder"; @@ -53,11 +58,11 @@ public class MultiFormatMediaTypeProcessorTest extends VertxTest { @Mock private BidderAliases bidderAliases; - private MultiFormatMediaTypeProcessor target; + private BidderRequestPreferredMediaProcessor target; @BeforeEach public void setUp() { - target = new MultiFormatMediaTypeProcessor(bidderCatalog); + target = new BidderRequestPreferredMediaProcessor(bidderCatalog); when(bidderAliases.resolveBidder(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); } @@ -65,14 +70,13 @@ public void setUp() { public void processShouldReturnSameBidRequestIfMultiFormatSupported() { // given given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(true)); - final BidRequest bidRequest = givenBidRequest(identity(), givenImp(BANNER, VIDEO)); + final BidderRequest bidderRequest = givenBidderRequest(identity(), givenImp(BANNER, VIDEO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, null); + final Result result = target.process(bidderRequest, bidderAliases, null).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()).isEqualTo(bidRequest); + assertThat(result.getValue()).isEqualTo(bidderRequest); assertThat(result.getErrors()).isEmpty(); } @@ -80,15 +84,14 @@ public void processShouldReturnSameBidRequestIfMultiFormatSupported() { public void processShouldReturnSameBidRequestIfPreferredMediaTypeNotSpecified() { // given given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(false)); - final BidRequest bidRequest = givenBidRequest(identity(), givenImp(BANNER, VIDEO)); - final Account account = givenAccount(null); + final BidderRequest bidderRequest = givenBidderRequest(identity(), givenImp(BANNER, VIDEO)); + final AuctionContext auctionContext = givenAuctionContext(null); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Result result = target.process(bidderRequest, bidderAliases, auctionContext).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()).isEqualTo(bidRequest); + assertThat(result.getValue()).isEqualTo(bidderRequest); assertThat(result.getErrors()).isEmpty(); } @@ -96,15 +99,15 @@ public void processShouldReturnSameBidRequestIfPreferredMediaTypeNotSpecified() public void processShouldReturnImpWithPreferredMediaType() { // given given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(false)); - final BidRequest bidRequest = givenBidRequest(identity(), givenImp(BANNER, VIDEO, AUDIO, NATIVE)); - final Account account = givenAccount(Map.of(BIDDER, VIDEO)); + final BidderRequest bidderRequest = givenBidderRequest(identity(), givenImp(BANNER, VIDEO, AUDIO, NATIVE)); + final AuctionContext auctionContext = givenAuctionContext(Map.of(BIDDER, VIDEO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Result result = target.process(bidderRequest, bidderAliases, auctionContext).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) .containsExactly(givenImp(VIDEO)); @@ -120,28 +123,23 @@ public void processShouldUseRequestLevelPreferredMediaTypeFirst() { final ObjectNode bidderControls = mapper.createObjectNode(); bidderControls.putObject(BIDDER).put("prefmtype", "video"); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() .biddercontrols(bidderControls) .build())), givenImp(BANNER, VIDEO, AUDIO, NATIVE)); - final Account account = givenAccount(Map.of("resolvedBidderName", AUDIO)); + final AuctionContext auctionContext = givenAuctionContext(Map.of("resolvedBidderName", AUDIO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Result result = target.process(bidderRequest, bidderAliases, auctionContext).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) .containsExactly(givenImp(VIDEO)); - assertThat(result.getBidRequest()) - .extracting(BidRequest::getExt) - .extracting(ExtRequest::getPrebid) - .extracting(ExtRequestPrebid::getBiddercontrols) - .isNull(); assertThat(result.getErrors()).isEmpty(); } @@ -154,28 +152,23 @@ public void processShouldUseRequestLevelPreferredMediaTypeFirstCaseInsensitive() final ObjectNode bidderControls = mapper.createObjectNode(); bidderControls.putObject(BIDDER.toUpperCase()).put("prefmtype", "video"); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() .biddercontrols(bidderControls) .build())), givenImp(BANNER, VIDEO, AUDIO, NATIVE)); - final Account account = givenAccount(Map.of("resolvedBidderName", AUDIO)); + final AuctionContext auctionContext = givenAuctionContext(Map.of("resolvedBidderName", AUDIO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Result result = target.process(bidderRequest, bidderAliases, auctionContext).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) .containsExactly(givenImp(VIDEO)); - assertThat(result.getBidRequest()) - .extracting(BidRequest::getExt) - .extracting(ExtRequest::getPrebid) - .extracting(ExtRequestPrebid::getBiddercontrols) - .isNull(); assertThat(result.getErrors()).isEmpty(); } @@ -183,21 +176,21 @@ public void processShouldUseRequestLevelPreferredMediaTypeFirstCaseInsensitive() public void processShouldSkipImpsWithSingleMediaType() { // given given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(false)); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( identity(), givenImp(BANNER), givenImp(VIDEO), givenImp(BANNER, VIDEO, AUDIO, NATIVE), givenImp(AUDIO), givenImp(NATIVE)); - final Account account = givenAccount(Map.of(BIDDER, VIDEO)); + final AuctionContext auctionContext = givenAuctionContext(Map.of(BIDDER, VIDEO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Result result = target.process(bidderRequest, bidderAliases, auctionContext).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) .containsExactly( @@ -213,7 +206,7 @@ public void processShouldSkipImpsWithSingleMediaType() { public void processShouldFilterMultiFormatImpsWithoutPreferredMediaType() { // given given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(false)); - final BidRequest bidRequest = givenBidRequest( + final BidderRequest bidderRequest = givenBidderRequest( identity(), givenImp(BANNER, AUDIO), givenImp(BANNER, VIDEO), @@ -221,14 +214,14 @@ public void processShouldFilterMultiFormatImpsWithoutPreferredMediaType() { givenImp(VIDEO, AUDIO), givenImp(VIDEO, NATIVE), givenImp(AUDIO, NATIVE)); - final Account account = givenAccount(Map.of(BIDDER, VIDEO)); + final AuctionContext auctionContext = givenAuctionContext(Map.of(BIDDER, VIDEO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Result result = target.process(bidderRequest, bidderAliases, auctionContext).result(); // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()) + assertThat(result.getValue()) + .extracting(BidderRequest::getBidRequest) .extracting(BidRequest::getImp) .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) .containsExactly( @@ -248,18 +241,24 @@ public void processShouldFilterMultiFormatImpsWithoutPreferredMediaType() { public void processShouldRejectEmptyRequestAfterFiltering() { // given given(bidderCatalog.bidderInfoByName(BIDDER)).willReturn(givenBidderInfo(false)); - final BidRequest bidRequest = givenBidRequest(identity(), givenImp(BANNER, AUDIO, NATIVE)); - final Account account = givenAccount(Map.of(BIDDER, VIDEO)); + final BidderRequest bidderRequest = givenBidderRequest(identity(), givenImp(BANNER, AUDIO, NATIVE)); + final AuctionContext auctionContext = givenAuctionContext(Map.of(BIDDER, VIDEO)); // when - final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + final Future> result = target.process(bidderRequest, bidderAliases, auctionContext); // then - assertThat(result.isRejected()).isTrue(); - assertThat(result.getErrors()).containsExactly( - BidderError.badInput("Imp null does not have a media type after filtering" - + " and has been removed from the request for this bidder."), - BidderError.badInput("Bid request contains 0 impressions after filtering.")); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()) + .asInstanceOf(InstanceOfAssertFactories.type(BidderRequestRejectedException.class)) + .satisfies(e -> { + assertThat(e.getRejectionReason()) + .isEqualTo(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE); + assertThat(e.getErrors()).containsExactly( + BidderError.badInput("Imp null does not have a media type after filtering" + + " and has been removed from the request for this bidder."), + BidderError.badInput("Bid request contains 0 impressions after filtering.")); + }); } private static BidderInfo givenBidderInfo(boolean multiFormatSupported) { @@ -283,10 +282,13 @@ private static BidderInfo givenBidderInfo(boolean multiFormatSupported) { 0L); } - private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, - Imp... imps) { + private static BidderRequest givenBidderRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { - return bidRequestCustomizer.apply(BidRequest.builder()).imp(asList(imps)).build(); + return BidderRequest.builder() + .bidder(BIDDER) + .bidRequest(bidRequestCustomizer.apply(BidRequest.builder()).imp(asList(imps)).build()) + .build(); } private static Imp givenImp(MediaType... mediaTypes) { @@ -309,10 +311,12 @@ private static Imp givenImp(MediaType... mediaTypes) { return impBuilder.build(); } - private static Account givenAccount(Map bidderToPreferredMediaType) { - return Account.builder() - .auction(AccountAuctionConfig.builder() - .preferredMediaTypes(bidderToPreferredMediaType) + private static AuctionContext givenAuctionContext(Map bidderToPreferredMediaType) { + return AuctionContext.builder() + .account(Account.builder() + .auction(AccountAuctionConfig.builder() + .preferredMediaTypes(bidderToPreferredMediaType) + .build()) .build()) .build(); } diff --git a/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessorTest.java b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessorTest.java new file mode 100644 index 00000000000..885025245db --- /dev/null +++ b/src/test/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessorTest.java @@ -0,0 +1,89 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +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.aliases.BidderAliases; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.Result; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CompositeBidderRequestPostProcessorTest { + + @Mock + private BidderRequestPostProcessor bidderRequestPostProcessor1; + + @Mock + private BidderRequestPostProcessor bidderRequestPostProcessor2; + + @Mock(strictness = LENIENT) + private BidderAliases bidderAliases; + + private CompositeBidderRequestPostProcessor target; + + @BeforeEach + public void setUp() { + target = new CompositeBidderRequestPostProcessor(asList( + bidderRequestPostProcessor1, bidderRequestPostProcessor2)); + when(bidderAliases.resolveBidder(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + public void processShouldReturnExpectedResult() { + // given + given(bidderRequestPostProcessor1.process(any(), any(), any())) + .willReturn(Future.succeededFuture(Result.of( + BidderRequest.builder().bidder("processed by bidderRequestPostProcessor1").build(), + singletonList(BidderError.badInput("Error from bidderRequestPostProcessor1"))))); + + given(bidderRequestPostProcessor2.process( + argThat(request -> "processed by bidderRequestPostProcessor1".equals(request.getBidder())), + any(), + any())) + .willReturn(Future.succeededFuture(Result.of( + BidderRequest.builder().bidder("processed by bidderRequestPostProcessor2").build(), + singletonList(BidderError.badInput("Error from bidderRequestPostProcessor2"))))); + + // when + final Future> result = target.process(null, bidderAliases, null); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result().getValue()) + .isEqualTo(BidderRequest.builder().bidder("processed by bidderRequestPostProcessor2").build()); + assertThat(result.result().getErrors()) + .containsExactly( + BidderError.badInput("Error from bidderRequestPostProcessor1"), + BidderError.badInput("Error from bidderRequestPostProcessor2")); + } + + @Test + public void processShouldReturnExpectedResultIfSomeOfProcessorsFails() { + // given + given(bidderRequestPostProcessor1.process(any(), any(), any())) + .willReturn(Future.failedFuture("Error from bidderRequestPostProcessor1")); + + // when + final Future> result = target.process(null, bidderAliases, null); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo("Error from bidderRequestPostProcessor1"); + verifyNoInteractions(bidderRequestPostProcessor2); + } +} diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessorTest.java deleted file mode 100644 index 47c40634788..00000000000 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessorTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.auction.aliases.BidderAliases; -import org.prebid.server.bidder.model.BidderError; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class CompositeMediaTypeProcessorTest { - - @Mock - private MediaTypeProcessor mediaTypeProcessor1; - - @Mock - private MediaTypeProcessor mediaTypeProcessor2; - - @Mock(strictness = LENIENT) - private BidderAliases bidderAliases; - - private CompositeMediaTypeProcessor target; - - @BeforeEach - public void setUp() { - target = new CompositeMediaTypeProcessor(asList(mediaTypeProcessor1, mediaTypeProcessor2)); - when(bidderAliases.resolveBidder(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); - } - - @Test - public void processShouldReturnExpectedResult() { - // given - given(mediaTypeProcessor1.process(any(), anyString(), any(), any())) - .willReturn(MediaTypeProcessingResult.succeeded( - BidRequest.builder().id("processed by mediaTypeProcessor1").build(), - singletonList(BidderError.badInput("Error from mediaTypeProcessor1")))); - - given(mediaTypeProcessor2.process( - argThat(request -> "processed by mediaTypeProcessor1".equals(request.getId())), - anyString(), - any(), - any())) - .willReturn(MediaTypeProcessingResult.succeeded( - BidRequest.builder().id("processed by mediaTypeProcessor2").build(), - singletonList(BidderError.badInput("Error from mediaTypeProcessor2")))); - - // when - final MediaTypeProcessingResult result = target.process( - BidRequest.builder().build(), - "bidder", - bidderAliases, - null); - - // then - assertThat(result.isRejected()).isFalse(); - assertThat(result.getBidRequest()) - .isEqualTo(BidRequest.builder().id("processed by mediaTypeProcessor2").build()); - assertThat(result.getErrors()) - .containsExactly( - BidderError.badInput("Error from mediaTypeProcessor1"), - BidderError.badInput("Error from mediaTypeProcessor2")); - } - - @Test - public void processShouldReturnExpectedResultIfRejectedBySomeOfProcessors() { - // given - given(mediaTypeProcessor1.process(any(), anyString(), any(), any())) - .willReturn(MediaTypeProcessingResult.rejected( - singletonList(BidderError.badInput("Error from mediaTypeProcessor1")))); - - // when - final MediaTypeProcessingResult result = target.process( - BidRequest.builder().build(), - "bidder", - bidderAliases, - null); - - // then - assertThat(result.isRejected()).isTrue(); - assertThat(result.getErrors()).containsExactly(BidderError.badInput("Error from mediaTypeProcessor1")); - verifyNoInteractions(mediaTypeProcessor2); - } -}