From cfbb6fb21ba5593ce113cf103bc4130f7008364b Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Wed, 4 Sep 2024 11:49:12 +0200 Subject: [PATCH] Core: Bidder accepted currency functionality (#3416) --- .../Ortb2BlockingBidderRequestHookTest.java | 1 + .../server/auction/ExchangeService.java | 152 +++------- .../auction/model/BidRejectionReason.java | 5 + .../org/prebid/server/bidder/BidderInfo.java | 4 + .../spring/config/bidder/model/MetaInfo.java | 2 + .../config/bidder/util/BidderInfoCreator.java | 1 + src/main/resources/bidder-config/generic.yaml | 6 +- .../auction/BidRejectionReason.groovy | 1 + .../functional/tests/BidderParamsSpec.groovy | 273 ++++++++++++++++-- .../server/auction/ExchangeServiceTest.java | 59 ++++ .../BidderMediaTypeProcessorTest.java | 1 + .../MultiFormatMediaTypeProcessorTest.java | 1 + .../enforcement/CcpaEnforcementTest.java | 2 + .../server/bidder/BidderCatalogTest.java | 8 + .../bidder/HttpBidderRequestEnricherTest.java | 2 + .../info/BidderDetailsHandlerTest.java | 1 + .../validation/BidderParamValidatorTest.java | 1 + 17 files changed, 388 insertions(+), 132 deletions(-) diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index a66f94ac52c..cb4e793f4fc 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -258,6 +258,7 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) { null, null, 0, + null, false, false, null, diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 8ccada983ac..2dbf1a6528f 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -50,6 +50,7 @@ import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.HttpBidderRequester; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.bidder.model.BidderBid; @@ -296,7 +297,7 @@ private Future runAuction(AuctionContext receivedContext) { .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses)) .compose(storedResponseResult -> extractAuctionParticipations(receivedContext, storedResponseResult, aliases, bidderToMultiBid) - .map(receivedContext::with)) + .map(receivedContext::with)) .map(context -> updateRequestMetric(context, uidsCookie, aliases, account, requestTypeMetric)) .compose(context -> CompositeFuture.join( @@ -470,44 +471,12 @@ private Map makeBidRejectionTrackers(BidRequest bid entry -> new BidRejectionTracker(entry.getKey(), entry.getValue(), logSamplingRate))); } - /** - * Populates storedResponse parameter with stored {@link List} and returns {@link List} for which - * request to bidders should be performed. - */ private static StoredResponseResult populateStoredResponse(StoredResponseResult storedResponseResult, List storedResponse) { storedResponse.addAll(storedResponseResult.getAuctionStoredResponse()); return storedResponseResult; } - /** - * Takes an OpenRTB request and returns the OpenRTB requests sanitized for each bidder. - *

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

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

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

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

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

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

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

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

- * Also, removes user.ext.prebid (if present), user.ext.data and user.data (in case bidder does not use first - * party data). - */ private User prepareUser(String bidder, AuctionContext context, BidderAliases aliases, @@ -734,9 +671,6 @@ private List extractUserEids(User user) { return user != null ? user.getEids() : null; } - /** - * Returns {@link List} allowed by {@param eidPermissions} per source per bidder. - */ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions) { return CollectionUtils.emptyIfNull(userEids) .stream() @@ -744,10 +678,6 @@ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions, String bidder) { final List allowedBidders = eidPermissions.get(source); return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() @@ -755,9 +685,6 @@ private boolean isUserEidAllowed(String source, Map> eidPer || EID_ALLOWED_FOR_ALL_BIDDERS.equals(allowedBidder)); } - /** - * Returns shuffled list of {@link AuctionParticipation} with {@link BidRequest}. - */ private List getAuctionParticipation( List bidderPrivacyResults, BidRequest bidRequest, @@ -810,9 +737,6 @@ private static Map bidderToPrebidBidders(BidRequest bidRequest return bidderToPrebidParameters; } - /** - * Returns {@link AuctionParticipation} for the given bidder. - */ private AuctionParticipation createAuctionParticipation( BidderPrivacyResult bidderPrivacyResult, Map> impBidderToStoredBidResponse, @@ -1237,16 +1161,20 @@ private Future processAndRequestBids(AuctionContext auctionConte final String bidderName = bidderRequest.getBidder(); final MediaTypeProcessingResult mediaTypeProcessingResult = mediaTypeProcessor.process( bidderRequest.getBidRequest(), bidderName, aliases, auctionContext.getAccount()); - final List mediaTypeProcessingErrors = mediaTypeProcessingResult.getErrors(); if (mediaTypeProcessingResult.isRejected()) { - auctionContext.getBidRejectionTrackers() - .get(bidderName) - .rejectAll(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE); - final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() - .warnings(mediaTypeProcessingErrors) - .build(); - return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, + mediaTypeProcessingErrors, + bidderName); + } + if (isUnacceptableCurrency(auctionContext, aliases.resolveBidder(bidderName))) { + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, + List.of(BidderError.generic("No match between the configured currencies and bidRequest.cur")), + bidderName); } return Future.succeededFuture(mediaTypeProcessingResult.getBidRequest()) @@ -1257,6 +1185,34 @@ private Future processAndRequestBids(AuctionContext auctionConte addWarnings(bidderResponse.getSeatBid(), mediaTypeProcessingErrors))); } + private boolean isUnacceptableCurrency(AuctionContext auctionContext, String originalBidderName) { + final List requestCurrencies = auctionContext.getBidRequest().getCur(); + final List bidAcceptableCurrencies = + Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) + .map(BidderInfo::getCurrencyAccepted) + .orElse(null); + + if (CollectionUtils.isEmpty(requestCurrencies) || CollectionUtils.isEmpty(bidAcceptableCurrencies)) { + return false; + } + + return !CollectionUtils.containsAny(requestCurrencies, bidAcceptableCurrencies); + } + + private static Future processReject(AuctionContext auctionContext, + BidRejectionReason bidRejectionReason, + List warnings, + String bidderName) { + + auctionContext.getBidRejectionTrackers() + .get(bidderName) + .rejectAll(bidRejectionReason); + final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() + .warnings(warnings) + .build(); + return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + } + private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { return CollectionUtils.isNotEmpty(warnings) ? seatBid.toBuilder() @@ -1416,13 +1372,6 @@ private List validateAndAdjustBids(List - * Removes invalid bids from response and adds corresponding error to {@link BidderSeatBid}. - *

- * Returns input argument as the result if no errors found or creates new {@link BidderResponse} otherwise. - */ private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, AuctionContext auctionContext, BidderAliases aliases) { @@ -1483,13 +1432,6 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); } - /** - * Performs changes on {@link Bid}s price depends on different between adServerCurrency and bidCurrency, - * and adjustment factor. Will drop bid if currency conversion is needed but not possible. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, BidRequest bidRequest) { if (auctionParticipation.isRequestBlocked()) { @@ -1594,13 +1536,6 @@ private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } - /** - * Updates 'request_time', 'responseTime', 'timeout_request', 'error_requests', 'no_bid_requests', - * 'prices' metrics for each {@link AuctionParticipation}. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ private List updateResponsesMetrics(List auctionParticipations, Account account, BidderAliases aliases) { @@ -1649,9 +1584,6 @@ private Future invokeResponseHooks(AuctionContext auctionContext .map(auctionContext::with); } - /** - * Resolves {@link MetricName} by {@link BidderError.Type} value. - */ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { return switch (errorType) { case bad_input -> MetricName.badinput; diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index 70fd0244bc5..e916ee0b3a5 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -58,6 +58,11 @@ public enum BidRejectionReason { */ REQUEST_BLOCKED_PRIVACY(204), + /** + * If the bidder was not called due to a mismatch between the bidder’s currency and the request’s currency. + */ + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), + /** * The bidder is called, but its response is rejected. * Applied if any other RESPONSE_REJECTED reason is not recognized. diff --git a/src/main/java/org/prebid/server/bidder/BidderInfo.java b/src/main/java/org/prebid/server/bidder/BidderInfo.java index fffbb1bab5b..c9659135eb7 100644 --- a/src/main/java/org/prebid/server/bidder/BidderInfo.java +++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java @@ -28,6 +28,8 @@ public class BidderInfo { List vendors; + List currencyAccepted; + GdprInfo gdpr; boolean ccpaEnforced; @@ -49,6 +51,7 @@ public static BidderInfo create(boolean enabled, List doohMediaTypes, List supportedVendors, int vendorId, + List currencyAccepted, boolean ccpaEnforced, boolean modifyingVastXmlAllowed, CompressionType compressionType, @@ -66,6 +69,7 @@ public static BidderInfo create(boolean enabled, platformInfo(siteMediaTypes), platformInfo(doohMediaTypes)), supportedVendors, + currencyAccepted, new GdprInfo(vendorId), ccpaEnforced, modifyingVastXmlAllowed, diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java index c25236dd799..bffc274c35d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java @@ -24,6 +24,8 @@ public class MetaInfo { private List supportedVendors; + private List currencyAccepted; + @NotNull private Integer vendorId; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java index 342ce998592..cd7553bb34a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java @@ -28,6 +28,7 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope metaInfo.getDoohMediaTypes(), metaInfo.getSupportedVendors(), metaInfo.getVendorId(), + metaInfo.getCurrencyAccepted(), configurationProperties.getPbsEnforcesCcpa(), configurationProperties.getModifyingVastXmlAllowed(), configurationProperties.getEndpointCompression(), diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index b52c7ac21b1..2c15fd531dd 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -114,9 +114,9 @@ adapters: - video - native site-media-types: - - banner - - video - - native + - banner + - video + - native supported-vendors: vendor-id: 263 usersync: diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index 3f14bac3db1..79cf8ad9317 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy @@ -14,6 +14,7 @@ enum BidRejectionReason { REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), REQUEST_BLOCKED_PRIVACY(204), + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), RESPONSE_REJECTED_GENERAL(300), RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR(301), diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index 8b087120db0..cab50bd816b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredImp @@ -16,19 +17,20 @@ import org.prebid.server.functional.model.request.auction.ImpExtContext import org.prebid.server.functional.model.request.auction.ImpExtContextData import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidStoredRequest -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent +import static org.prebid.server.functional.model.Currency.CHF +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.CompressionType.GZIP import static org.prebid.server.functional.model.bidder.CompressionType.NONE import static org.prebid.server.functional.model.request.auction.Asset.titleAsset @@ -37,11 +39,15 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY +import static org.prebid.server.functional.model.response.auction.ErrorType.ALIAS +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.HttpUtil.CONTENT_ENCODING_HEADER import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED @@ -58,7 +64,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain httpcalls" - assert response.ext?.debug?.httpcalls[GENERIC.value] + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] and: "Response should not contain error" assert !response.ext?.errors @@ -84,7 +90,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [2] + assert response.ext?.errors[GENERIC]*.code == [2] where: adapterDefault | generic | adapterConfig @@ -212,7 +218,7 @@ class BidderParamsSpec extends BaseSpec { bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(firstParam: firstParam) and: "Set bidderParam to bidRequest" - bidRequest.ext.prebid.bidderParams = [(GENERIC): [firstParam: PBSUtils.randomNumber]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [firstParam: PBSUtils.randomNumber]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -247,7 +253,7 @@ class BidderParamsSpec extends BaseSpec { and: "Set bidderParam to bidRequest" def secondParam = PBSUtils.randomNumber - bidRequest.ext.prebid.bidderParams = [(GENERIC): [secondParam: secondParam]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [secondParam: secondParam]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -289,8 +295,8 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [999] - assert response.ext?.errors[ErrorType.GENERIC]*.message == ["host name must not be empty"] + assert response.ext?.errors[GENERIC]*.code == [999] + assert response.ext?.errors[GENERIC]*.message == ["host name must not be empty"] } def "PBS should reject bidder when bidder params from request doesn't satisfy json-schema for auction request"() { @@ -395,8 +401,8 @@ class BidderParamsSpec extends BaseSpec { assert response.seatbid.isEmpty() and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == ["Bidder does not support any media types."] + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Bidder does not support any media types."] where: configMediaType | bidRequest @@ -512,8 +518,8 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequest.imp[0].nativeObj and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from the " + "request for this bidder." as String] @@ -531,7 +537,7 @@ class BidderParamsSpec extends BaseSpec { def bidResponse = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert bidResponse.ext?.warnings[ErrorType.GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") + assert bidResponse.ext?.warnings[GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") and: "Bid response shouldn't contain any seatbid" assert !bidResponse.seatbid @@ -565,7 +571,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert response.ext?.warnings[ErrorType.GENERIC]?.message == + assert response.ext?.warnings[GENERIC]?.message == ["Imp ${bidRequest.imp[1].id} does not have a supported media type and has been removed from the request for this bidder."] and: "Bid response should contain seatbid" @@ -600,8 +606,8 @@ class BidderParamsSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 0 and: "Response should contain errors" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2, 2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2, 2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from " + "the request for this bidder.", "Bid request contains 0 impressions after filtering."] @@ -646,7 +652,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain header Content-Encoding = gzip" - assert response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER)?.first() == compressionType } @@ -662,7 +668,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should not contain header Content-Encoding" - assert !response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert !response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER) } @@ -805,4 +811,233 @@ class BidderParamsSpec extends BaseSpec { tid == impExt.tid } } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "") + + and: "Default bid request with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "") + + and: "Default bid request with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + and: "PBS should emit an warnings" + assert response.ext?.warnings[GENERIC]*.code == [999] + assert response.ext?.warnings[GENERIC]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.aliases.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "PBS should emit an warnings" + assert response.ext?.warnings[ALIAS]*.code == [999] + assert response.ext?.warnings[ALIAS]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.ALIAS.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index ffff9684fb1..0c7710dbc74 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -48,6 +48,8 @@ import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; @@ -329,6 +331,7 @@ public void setUp() { null, null, 0, + null, false, false, CompressionType.NONE, @@ -4668,6 +4671,62 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() .isEqualTo(BidResponse.builder().id("uniqId").build()); } + @Test + public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() { + // given + final Imp imp = givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")); + final BidRequest bidRequest = givenBidRequest(singletonList(imp), + bidRequestBuilder -> bidRequestBuilder.cur(singletonList("USD"))); + final AuctionContext auctionContext = givenRequestContext(bidRequest); + + given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create( + true, + null, + false, + null, + null, + null, + null, + null, + null, + null, + 0, + singletonList("CAD"), + false, + false, + CompressionType.NONE, + Ortb.of(false))); + given(bidResponseCreator.create( + argThat(argument -> argument.getAuctionParticipations().getFirst() + .getBidderResponse() + .equals(BidderResponse.of( + "bidder1", + BidderSeatBid.builder() + .warnings(Collections.singletonList( + BidderError.generic( + "No match between the configured currencies and bidRequest.cur" + ))) + .build(), + 0))), + any(), + any())) + .willReturn(Future.succeededFuture(BidResponse.builder().id("uniqId").build())); + + // when + final Future result = target.holdAuction(auctionContext); + + // then + assertThat(result.result()) + .extracting(AuctionContext::getBidResponse) + .isEqualTo(BidResponse.builder().id("uniqId").build()); + assertThat(result.result()) + .extracting(AuctionContext::getBidRejectionTrackers) + .extracting(rejectionTrackers -> rejectionTrackers.get("bidder1")) + .extracting(BidRejectionTracker::getRejectionReasons) + .isEqualTo(Map.of("impId1", BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY)); + + } + @Test public void shouldConvertBidRequestOpenRTBVersionToConfiguredByBidder() { // given diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java index 8103416ef22..60ff60318e1 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java @@ -171,6 +171,7 @@ private static BidderInfo givenBidderInfo(List appMediaTypes, doohMediaType, emptyList(), 0, + null, false, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java index 2e0e0422450..e2394769585 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java @@ -275,6 +275,7 @@ private static BidderInfo givenBidderInfo(boolean multiFormatSupported) { emptyList(), emptyList(), 0, + emptyList(), false, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java index b8287e6c086..5459e232386 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java @@ -76,6 +76,7 @@ public void setUp() { null, null, 0, + null, true, false, null, @@ -213,6 +214,7 @@ public void enforceShouldSkipNoSaleBiddersAndNotEnforcedByBidderConfig() { null, null, 0, + null, false, false, null, diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java index 6fbb3546a02..f2e0207fc4a 100644 --- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java +++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java @@ -95,6 +95,7 @@ public void metaInfoByNameShouldReturnMetaInfoForKnownBidderIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -127,6 +128,7 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -150,6 +152,7 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -186,6 +189,7 @@ public void resolveBaseBidderShouldReturnBaseBidderName() { emptyList(), null, 0, + null, true, false, CompressionType.NONE, @@ -252,6 +256,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -269,6 +274,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -286,6 +292,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -354,6 +361,7 @@ public void nameByVendorIdShouldReturnBidderNameForVendorId() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java index 440ba94cd2f..38cad500e7b 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java @@ -170,6 +170,7 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderConfig() { null, null, 0, + null, false, false, CompressionType.GZIP, @@ -207,6 +208,7 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderAliasConfig() { null, null, 0, + null, false, false, CompressionType.GZIP, diff --git a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java index e78e65fa082..d8589113f31 100644 --- a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java @@ -195,6 +195,7 @@ private static BidderInfo givenBidderInfo(boolean enabled, String endpoint, Stri singletonList(MediaType.NATIVE), null, 0, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java index 863ce3bb53b..a14c6141355 100644 --- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java @@ -394,6 +394,7 @@ private static BidderInfo givenBidderInfo(String aliasOf) { null, null, 0, + null, true, false, CompressionType.NONE,