diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 6fc7a23a818..7fcacb1cd54 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT ../../extra/pom.xml @@ -77,12 +77,12 @@ org.prebid prebid-server - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT org.prebid.server.hooks.modules ortb2-blocking - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 45feecb615c..cfd74cc0189 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index b95b66f88b3..413bbe5e1ec 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT ../../extra/pom.xml @@ -26,7 +26,7 @@ 11 11 - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT 1.18.22 diff --git a/extra/pom.xml b/extra/pom.xml index ee001d5264d..4b9691068e5 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT pom diff --git a/pom.xml b/pom.xml index 47642d84a3e..12162b85614 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.88.0-SNAPSHOT extra/pom.xml diff --git a/src/main/java/org/prebid/server/bidder/model/BidderError.java b/src/main/java/org/prebid/server/bidder/model/BidderError.java index 3f3facd0cc4..620874ece50 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderError.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderError.java @@ -34,6 +34,10 @@ public static BidderError badServerResponse(String message) { return BidderError.of(message, Type.bad_server_response); } + public static BidderError rejectedIpf(String message) { + return BidderError.of(message, Type.rejected_ipf); + } + public static BidderError failedToRequestBids(String message) { return BidderError.of(message, Type.failed_to_request_bids); } @@ -79,6 +83,11 @@ public enum Type { */ invalid_bid(5), + /** + * Covers the case where a bid was rejected by price-floors feature functionality + */ + rejected_ipf(6), + timeout(1), generic(999); diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 8a1c38c3235..a9136e90886 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -38,6 +38,7 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.PriceFloorInfo; import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.rubicon.proto.request.RubiconAppExt; import org.prebid.server.bidder.rubicon.proto.request.RubiconBannerExt; @@ -67,8 +68,6 @@ import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.PriceFloorResolver; -import org.prebid.server.floors.model.PriceFloorData; -import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.json.DecodeException; @@ -418,14 +417,31 @@ private Imp makeImp(Imp imp, final PriceFloorResult priceFloorResult = resolvePriceFloors(bidRequest, imp, isVideo ? ImpMediaType.video : ImpMediaType.banner, priceFloorsWarnings); + final BigDecimal ipfFloor = ObjectUtil.getIfNotNull(priceFloorResult, PriceFloorResult::getFloorValue); + final String ipfCurrency = ipfFloor != null + ? resolveCurrencyFromFloorResult( + ObjectUtil.getIfNotNull(priceFloorResult, PriceFloorResult::getCurrency), + bidRequest, + imp, + errors) + : null; + final Imp.ImpBuilder builder = imp.toBuilder() .metric(makeMetrics(imp)) .ext(mapper.mapper().valueToTree( - makeImpExt(imp, extImpRubicon, site, app, extRequest, priceFloorResult))); - - final BigDecimal resolvedBidFloor = priceFloorResult != null - ? resolvePriceFloorBidFloor(imp, bidRequest, priceFloorResult, errors) - : resolveBidFloor(imp, bidRequest, errors); + makeImpExt( + imp, + bidRequest, + extImpRubicon, + site, + app, + extRequest, + ipfCurrency, + priceFloorResult))); + + final BigDecimal resolvedBidFloor = ipfFloor != null + ? convertToXAPICurrency(ipfFloor, ipfCurrency, imp, bidRequest) + : resolveBidFloorFromImp(imp, bidRequest, errors); if (resolvedBidFloor != null) { builder @@ -455,21 +471,16 @@ private PriceFloorResult resolvePriceFloors(BidRequest bidRequest, return floorResolver.resolve( bidRequest, - extractFloorModelGroup(bidRequest), + extractFloorRules(bidRequest), imp, mediaType, null, warnings); } - private static PriceFloorModelGroup extractFloorModelGroup(BidRequest bidRequest) { + private static PriceFloorRules extractFloorRules(BidRequest bidRequest) { final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); - final PriceFloorRules floorRules = ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); - - final PriceFloorData data = ObjectUtil.getIfNotNull(floorRules, PriceFloorRules::getData); - final List modelGroups = ObjectUtil.getIfNotNull(data, PriceFloorData::getModelGroups); - - return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.get(0) : null; + return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); } private List makeMetrics(Imp imp) { @@ -495,31 +506,27 @@ private boolean isMetricSupported(Metric metric) { return supportedVendors.contains(metric.getVendor()) && Objects.equals(metric.getType(), "viewability"); } - private BigDecimal resolvePriceFloorBidFloor(Imp imp, - BidRequest bidRequest, - PriceFloorResult floorResult, - List errors) { - final BigDecimal floorValue = ObjectUtil.getIfNotNull(floorResult, PriceFloorResult::getFloorValue); - if (floorValue == null) { + private BigDecimal resolveBidFloorFromImp(Imp imp, BidRequest bidRequest, List errors) { + final BigDecimal resolvedBidFloorPrice = resolveBidFloorPrice(imp); + if (resolvedBidFloorPrice == null) { return null; } - final String floorCurrency = resolveCurrencyFromFloorResult(floorResult, bidRequest, imp, errors); - return ObjectUtils.notEqual(floorCurrency, XAPI_CURRENCY) - ? convertBidFloorCurrency(floorValue, floorCurrency, imp, bidRequest) - : null; + return convertToXAPICurrency( + resolvedBidFloorPrice, + resolveBidFloorCurrency(imp, bidRequest, errors), + imp, + bidRequest); } - private BigDecimal resolveBidFloor(Imp imp, BidRequest bidRequest, List errors) { - final BigDecimal resolvedBidFloorPrice = resolveBidFloorPrice(imp); - if (resolvedBidFloorPrice == null) { - return null; - } + private BigDecimal convertToXAPICurrency(BigDecimal value, + String fromCurrency, + Imp imp, + BidRequest bidRequest) { - final String resolvedBidFloorCurrency = resolveBidFloorCurrency(imp, bidRequest, errors); - return ObjectUtils.notEqual(resolvedBidFloorCurrency, XAPI_CURRENCY) - ? convertBidFloorCurrency(resolvedBidFloorPrice, resolvedBidFloorCurrency, imp, bidRequest) - : null; + return ObjectUtils.notEqual(fromCurrency, XAPI_CURRENCY) + ? convertBidFloorCurrency(value, fromCurrency, imp, bidRequest) + : value; } private static BigDecimal resolveBidFloorPrice(Imp imp) { @@ -539,20 +546,19 @@ private static String resolveBidFloorCurrency(Imp imp, BidRequest bidRequest, Li return bidFloorCurrency; } - private static String resolveCurrencyFromFloorResult(PriceFloorResult floorResult, + private static String resolveCurrencyFromFloorResult(String floorCurrency, BidRequest bidRequest, Imp imp, List errors) { - final String bidFloorCurrency = floorResult.getCurrency(); - if (StringUtils.isBlank(bidFloorCurrency)) { + if (StringUtils.isBlank(floorCurrency)) { if (isDebugEnabled(bidRequest)) { - errors.add(BidderError.badInput(String.format("Imp `%s` floor provided with no currency, assuming %s", - imp.getId(), XAPI_CURRENCY))); + errors.add(BidderError.badInput(String.format("Ipf for imp `%s` provided floor with no currency, " + + "assuming %s", imp.getId(), XAPI_CURRENCY))); } return XAPI_CURRENCY; } - return bidFloorCurrency; + return floorCurrency; } /** @@ -581,15 +587,17 @@ private BigDecimal convertBidFloorCurrency(BigDecimal bidFloor, } private RubiconImpExt makeImpExt(Imp imp, + BidRequest bidRequest, ExtImpRubicon rubiconImpExt, Site site, App app, ExtRequest extRequest, + String ipfResolvedCurrency, PriceFloorResult priceFloorResult) { final ExtImpContext context = extImpContext(imp); final RubiconImpExtPrebid rubiconImpExtPrebid = priceFloorResult != null - ? makeRubiconExtPrebid(priceFloorResult) + ? makeRubiconExtPrebid(priceFloorResult, ipfResolvedCurrency, imp, bidRequest) : null; return RubiconImpExt.of( RubiconImpExtRp.of( @@ -626,11 +634,14 @@ private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App return result.size() > 0 ? result : null; } - private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult) { + private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult, + String currency, + Imp imp, + BidRequest bidRequest) { return RubiconImpExtPrebid.of(ExtImpPrebidFloors.of( priceFloorResult.getFloorRule(), - priceFloorResult.getFloorRuleValue(), - priceFloorResult.getFloorValue())); + convertToXAPICurrency(priceFloorResult.getFloorRuleValue(), currency, imp, bidRequest), + convertToXAPICurrency(priceFloorResult.getFloorValue(), currency, imp, bidRequest))); } private void mergeFirstPartyDataFromSite(Site site, ObjectNode result) { @@ -1477,6 +1488,8 @@ private List bidsFromResponse(BidRequest prebidRequest, List errors) { final Map idToImp = prebidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, Function.identity())); + final Map idToRubiconImp = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); final Float cpmOverrideFromRequest = cpmOverrideFromRequest(prebidRequest); final BidType bidType = bidType(bidRequest); @@ -1487,7 +1500,7 @@ private List bidsFromResponse(BidRequest prebidRequest, .filter(Objects::nonNull) .flatMap(Collection::stream) .map(bid -> updateBid(bid, idToImp.get(bid.getImpid()), cpmOverrideFromRequest, bidResponse)) - .map(bid -> BidderBid.of(bid, bidType, bidResponse.getCur())) + .map(bid -> createBidderBid(bid, idToRubiconImp.get(bid.getImpid()), bidType, bidResponse.getCur())) .collect(Collectors.toList()); } @@ -1561,6 +1574,16 @@ private Bid updateBid(Bid bid, Imp imp, Float cpmOverrideFromRequest, RubiconBid .build(); } + private static BidderBid createBidderBid(Bid bid, Imp imp, BidType bidType, String currency) { + + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(currency) + .priceFloorInfo(imp != null ? PriceFloorInfo.of(imp.getBidfloor(), imp.getBidfloorcur()) : null) + .build(); + } + private Float cpmOverrideFromRequest(BidRequest bidRequest) { final RubiconExtPrebidBiddersBidder bidder = extPrebidBiddersRubicon(bidRequest.getExt()); final RubiconExtPrebidBiddersBidderDebug debug = bidder != null ? bidder.getDebug() : null; diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 70c2077f226..07901ed704c 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -135,7 +135,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, final List updatedBidderBids = new ArrayList<>(bidderBids); final List errors = new ArrayList<>(seatBid.getErrors()); - final List warnings = new ArrayList<>(seatBid.getErrors()); + final List warnings = new ArrayList<>(seatBid.getWarnings()); final BidRequest bidderBidRequest = auctionParticipation.getBidderRequest().getBidRequest(); final PriceFloorRules floors = extractFloors(auctionParticipation); @@ -153,7 +153,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, final BigDecimal floor = resolveFloor(bidderBid, bidderBidRequest, bidRequest, errors); if (isPriceBelowFloor(price, floor)) { - warnings.add(BidderError.generic( + warnings.add(BidderError.rejectedIpf( String.format("Bid with id '%s' was rejected by floor enforcement: " + "price %s is below the floor %s", bid.getId(), price, floor))); diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index eff08b6b360..deb987d3cd2 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -4,15 +4,16 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.model.Price; -import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorData; -import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorLocation; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; @@ -20,6 +21,7 @@ import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; @@ -39,24 +41,24 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { + private static final Logger logger = LoggerFactory.getLogger(BasicPriceFloorProcessor.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; - private static final int MODEL_WEIGHT_MAX_VALUE = 1_000_000; - private static final int MODEL_WEIGHT_MIN_VALUE = 0; + private static final int MODEL_WEIGHT_MAX_VALUE = 100; + private static final int MODEL_WEIGHT_MIN_VALUE = 1; private final PriceFloorFetcher floorFetcher; private final PriceFloorResolver floorResolver; - private final CurrencyConversionService conversionService; private final JacksonMapper mapper; public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher, PriceFloorResolver floorResolver, - CurrencyConversionService conversionService, JacksonMapper mapper) { this.floorFetcher = Objects.requireNonNull(floorFetcher); this.floorResolver = Objects.requireNonNull(floorResolver); - this.conversionService = Objects.requireNonNull(conversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -68,10 +70,10 @@ public AuctionContext enrichWithPriceFloors(AuctionContext auctionContext) { final List warnings = auctionContext.getDebugWarnings(); if (isPriceFloorsDisabled(account, bidRequest)) { - return auctionContext; + return auctionContext.with(disableFloorsForRequest(bidRequest)); } - final PriceFloorRules floors = resolveFloors(account, bidRequest); + final PriceFloorRules floors = resolveFloors(account, bidRequest, errors); final BidRequest updatedBidRequest = updateBidRequestWithFloors(bidRequest, floors, errors, warnings); return auctionContext.with(updatedBidRequest); @@ -81,6 +83,22 @@ private boolean isPriceFloorsDisabled(Account account, BidRequest bidRequest) { return isPriceFloorsDisabledForAccount(account) || isPriceFloorsDisabledForRequest(bidRequest); } + private static BidRequest disableFloorsForRequest(BidRequest bidRequest) { + final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); + final PriceFloorRules rules = ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); + + final PriceFloorRules updatedRules = (rules != null ? rules.toBuilder() : PriceFloorRules.builder()) + .enabled(false) + .build(); + final ExtRequestPrebid updatedPrebid = (prebid != null ? prebid.toBuilder() : ExtRequestPrebid.builder()) + .floors(updatedRules) + .build(); + + return bidRequest.toBuilder() + .ext(ExtRequest.of(updatedPrebid)) + .build(); + } + private static boolean isPriceFloorsDisabledForAccount(Account account) { final AccountPriceFloorsConfig priceFloors = ObjectUtil.getIfNotNull(account.getAuction(), AccountAuctionConfig::getPriceFloors); @@ -97,19 +115,30 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) { return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); } - private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest) { + private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List errors) { final PriceFloorRules requestFloors = extractRequestFloors(bidRequest); final FetchResult fetchResult = floorFetcher.fetch(account); final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus); if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) { - final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRules()); + final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData()); return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch); } if (requestFloors != null) { - return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); + try { + PriceFloorRulesValidator.validateRules(requestFloors, Integer.MAX_VALUE); + return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); + } catch (PreBidException e) { + errors.add(String.format("Failed to parse price floors from request," + + " with a reason : %s ", e.getMessage())); + conditionalLogger.error( + String.format("Failed to parse price floors from request with id: '%s'," + + " with a reason : %s ", + bidRequest.getId(), + e.getMessage()), 0.01d); + } } return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData); @@ -125,34 +154,18 @@ private static boolean shouldUseDynamicData(Account account) { } private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, - PriceFloorRules providerFloors) { - - final Boolean floorsEnabledByRequest = ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getEnabled); - final PriceFloorEnforcement floorsRequestEnforcement = - ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getEnforcement); - final Integer enforceRate = - ObjectUtil.getIfNotNull(floorsRequestEnforcement, PriceFloorEnforcement::getEnforceRate); - final Price floorMinPrice = resolveFloorMinPrice(requestFloors, providerFloors); - - if (floorsEnabledByRequest != null || enforceRate != null || floorMinPrice != null) { - final Boolean floorsEnabledByProvider = - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnabled); - final PriceFloorEnforcement floorsProviderEnforcement = - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnforcement); - - return (providerFloors != null ? providerFloors.toBuilder() : PriceFloorRules.builder()) - .floorMinCur(ObjectUtil.getIfNotNull(floorMinPrice, Price::getCurrency)) - .floorMin(ObjectUtil.getIfNotNull(floorMinPrice, Price::getValue)) - .enabled(resolveFloorsEnabled(floorsEnabledByRequest, floorsEnabledByProvider)) - .enforcement(resolveFloorsEnforcement(floorsProviderEnforcement, enforceRate)) - .build(); - } + PriceFloorData providerRulesData) { + + final Price floorMinPrice = resolveFloorMinPrice(requestFloors); - return providerFloors; + return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder()) + .floorMinCur(ObjectUtil.getIfNotNull(floorMinPrice, Price::getCurrency)) + .floorMin(ObjectUtil.getIfNotNull(floorMinPrice, Price::getValue)) + .data(providerRulesData) + .build(); } - private Price resolveFloorMinPrice(PriceFloorRules requestFloors, - PriceFloorRules providerFloors) { + private Price resolveFloorMinPrice(PriceFloorRules requestFloors) { final String requestDataCurrency = ObjectUtil.getIfNotNull( ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getData), PriceFloorData::getCurrency); @@ -161,66 +174,11 @@ private Price resolveFloorMinPrice(PriceFloorRules requestFloors, requestDataCurrency); final BigDecimal requestFloorMin = ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getFloorMin); - final String providerFloorMinCur = - ObjectUtil.getIfNotNull( - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getData), PriceFloorData::getCurrency); - final BigDecimal providerFloorMin = ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getFloorMin); - - if (StringUtils.isNotBlank(requestFloorMinCur)) { - if (BidderUtil.isValidPrice(requestFloorMin)) { - return Price.of(requestFloorMinCur, requestFloorMin); - } else if (BidderUtil.isValidPrice(providerFloorMin)) { - if (StringUtils.equals(providerFloorMinCur, requestFloorMinCur)) { - return Price.of(requestFloorMinCur, providerFloorMin); - } - - return Price.of( - requestFloorMinCur, - conversionService.convertCurrency( - providerFloorMin, - Collections.emptyMap(), - providerFloorMinCur, - requestFloorMinCur, - false)); - } - } - - if (StringUtils.isNotBlank(providerFloorMinCur)) { - if (BidderUtil.isValidPrice(requestFloorMin)) { - return Price.of( - requestFloorMinCur, - conversionService.convertCurrency( - requestFloorMin, - Collections.emptyMap(), - requestFloorMinCur, - providerFloorMinCur, - false)); - } - - return Price.of(requestFloorMinCur, providerFloorMin); + if (StringUtils.isNotBlank(requestFloorMinCur) && BidderUtil.isValidPrice(requestFloorMin)) { + return Price.of(requestFloorMinCur, requestFloorMin); } - return Price.of(null, ObjectUtils.firstNonNull(requestFloorMin, providerFloorMin)); - } - - private static Boolean resolveFloorsEnabled(Boolean enabledByRequest, Boolean enabledByProvider) { - if (BooleanUtils.isFalse(enabledByRequest) || BooleanUtils.isFalse(enabledByProvider)) { - return false; - } - - return ObjectUtils.defaultIfNull(enabledByRequest, enabledByProvider); - } - - private static PriceFloorEnforcement resolveFloorsEnforcement(PriceFloorEnforcement providerEnforcement, - Integer enforceRate) { - - if (enforceRate == null) { - return providerEnforcement; - } - - return (providerEnforcement != null ? providerEnforcement.toBuilder() : PriceFloorEnforcement.builder()) - .enforceRate(enforceRate) - .build(); + return Price.of(null, requestFloorMin); } private static PriceFloorRules createFloorsFrom(PriceFloorRules floors, @@ -231,6 +189,7 @@ private static PriceFloorRules createFloorsFrom(PriceFloorRules floors, final PriceFloorData updatedFloorData = floorData != null ? updateFloorData(floorData) : null; return (floors != null ? floors.toBuilder() : PriceFloorRules.builder()) + .floorProvider(resolveFloorProvider(floors)) .fetchStatus(fetchStatus) .location(location) .data(updatedFloorData) @@ -282,23 +241,34 @@ private static boolean isValidModelGroup(PriceFloorModelGroup modelGroup) { final Integer modelWeight = modelGroup.getModelWeight(); return modelWeight == null - || (modelWeight > MODEL_WEIGHT_MIN_VALUE && modelWeight < MODEL_WEIGHT_MAX_VALUE); + || (modelWeight >= MODEL_WEIGHT_MIN_VALUE && modelWeight <= MODEL_WEIGHT_MAX_VALUE); } private static int resolveModelGroupWeight(PriceFloorModelGroup modelGroup) { return ObjectUtils.defaultIfNull(modelGroup.getModelWeight(), 1); } + private static String resolveFloorProvider(PriceFloorRules rules) { + final PriceFloorData floorData = ObjectUtil.getIfNotNull(rules, PriceFloorRules::getData); + final String dataLevelProvider = ObjectUtil.getIfNotNull(floorData, PriceFloorData::getFloorProvider); + + return StringUtils.isNotBlank(dataLevelProvider) + ? dataLevelProvider + : ObjectUtil.getIfNotNull(rules, PriceFloorRules::getFloorProvider); + } + private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, PriceFloorRules floors, List errors, List warnings) { - final boolean skipFloors = shouldSkipFloors(floors); + + final Integer requestSkipRate = extractSkipRate(floors); + final boolean skipFloors = shouldSkipFloors(requestSkipRate); final List imps = skipFloors ? bidRequest.getImp() : updateImpsWithFloors(floors, bidRequest, errors, warnings); - final ExtRequest extRequest = updateExtRequestWithFloors(bidRequest, floors, skipFloors); + final ExtRequest extRequest = updateExtRequestWithFloors(bidRequest, floors, requestSkipRate, skipFloors); return bidRequest.toBuilder() .imp(imps) @@ -306,9 +276,7 @@ private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, .build(); } - private static boolean shouldSkipFloors(PriceFloorRules floors) { - final Integer skipRate = extractSkipRate(floors); - + private static boolean shouldSkipFloors(Integer skipRate) { return skipRate != null && ThreadLocalRandom.current().nextInt(SKIP_RATE_MAX) < skipRate; } @@ -337,7 +305,7 @@ private static boolean isValidSkipRate(Integer value) { return value != null && value >= SKIP_RATE_MIN && value <= SKIP_RATE_MAX; } - private List updateImpsWithFloors(PriceFloorRules accountFloors, + private List updateImpsWithFloors(PriceFloorRules effectiveFloors, BidRequest bidRequest, List errors, List warnings) { @@ -346,14 +314,15 @@ private List updateImpsWithFloors(PriceFloorRules accountFloors, final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); final PriceFloorRules floors = - ObjectUtils.defaultIfNull(accountFloors, ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors)); + ObjectUtils.defaultIfNull(effectiveFloors, + ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors)); final PriceFloorModelGroup modelGroup = extractFloorModelGroup(floors); if (modelGroup == null) { return imps; } return CollectionUtils.emptyIfNull(imps).stream() - .map(imp -> updateImpWithFloors(imp, modelGroup, bidRequest, errors, warnings)) + .map(imp -> updateImpWithFloors(imp, floors, bidRequest, errors, warnings)) .collect(Collectors.toList()); } @@ -365,14 +334,14 @@ private static PriceFloorModelGroup extractFloorModelGroup(PriceFloorRules floor } private Imp updateImpWithFloors(Imp imp, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, BidRequest bidRequest, List errors, List warnings) { final PriceFloorResult priceFloorResult; try { - priceFloorResult = floorResolver.resolve(bidRequest, modelGroup, imp, warnings); + priceFloorResult = floorResolver.resolve(bidRequest, floorRules, imp, warnings); } catch (IllegalStateException e) { errors.add("Cannot resolve bid floor, error: " + e.getMessage()); return imp; @@ -404,19 +373,29 @@ private ObjectNode updateImpExtWithFloors(ObjectNode ext, PriceFloorResult price private static ExtRequest updateExtRequestWithFloors(BidRequest bidRequest, PriceFloorRules floors, + Integer skipRate, boolean skipFloors) { final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); final ExtRequestPrebid updatedPrebid = (prebid != null ? prebid.toBuilder() : ExtRequestPrebid.builder()) - .floors(skipFloors ? skippedFloors(floors) : floors) + .floors(skipFloors ? skippedFloors(floors, skipRate) : enabledFloors(floors, skipRate)) .build(); return ExtRequest.of(updatedPrebid); } - private static PriceFloorRules skippedFloors(PriceFloorRules floors) { + private static PriceFloorRules enabledFloors(PriceFloorRules floors, Integer skipRate) { + return floors.toBuilder() + .skipRate(skipRate) + .enabled(true) + .build(); + } + + private static PriceFloorRules skippedFloors(PriceFloorRules floors, Integer skipRate) { return floors.toBuilder() + .skipRate(skipRate) + .enabled(false) .skipped(true) .build(); } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java index 167a83c6a27..3adb5e3398d 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java @@ -93,12 +93,14 @@ public BasicPriceFloorResolver(CurrencyConversionService currencyConversionServi @Override public PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, ImpMediaType mediaType, Format format, List warnings) { + final PriceFloorModelGroup modelGroup = extractFloorModelGroup(floorRules); + if (modelGroup == null) { return null; } @@ -124,7 +126,7 @@ public PriceFloorResult resolve(BidRequest bidRequest, final String modelGroupCurrency = modelGroup.getCurrency(); final String floorCurrency = StringUtils.isNotEmpty(modelGroupCurrency) ? modelGroupCurrency - : getDataCurrency(bidRequest); + : getDataCurrency(floorRules); try { return resolveResult(floor, rule, floorForRule, bidRequest, floorCurrency); @@ -143,6 +145,13 @@ public PriceFloorResult resolve(BidRequest bidRequest, return null; } + private static PriceFloorModelGroup extractFloorModelGroup(PriceFloorRules floors) { + final PriceFloorData data = ObjectUtil.getIfNotNull(floors, PriceFloorRules::getData); + final List modelGroups = ObjectUtil.getIfNotNull(data, PriceFloorData::getModelGroups); + + return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.get(0) : null; + } + private List> createRuleKey(PriceFloorSchema schema, BidRequest bidRequest, Imp imp, @@ -427,8 +436,7 @@ private static Map keysToLowerCase(Map map) { .collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue)); } - private static String getDataCurrency(BidRequest bidRequest) { - final PriceFloorRules rules = extractRules(bidRequest); + private static String getDataCurrency(PriceFloorRules rules) { final PriceFloorData data = ObjectUtil.getIfNotNull(rules, PriceFloorRules::getData); return ObjectUtil.getIfNotNull(data, PriceFloorData::getCurrency); diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index c2615369f36..ea583544e7e 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -9,8 +9,6 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import lombok.Value; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -19,8 +17,6 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; -import org.prebid.server.floors.model.PriceFloorModelGroup; -import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; @@ -38,7 +34,6 @@ import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; -import java.math.BigDecimal; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -90,11 +85,11 @@ public FetchResult fetch(Account account) { final AccountFetchContext accountFetchContext = fetchedData.get(account.getId()); return accountFetchContext != null - ? FetchResult.of(accountFetchContext.getRules(), accountFetchContext.getFetchStatus()) - : fetchPriceFloorRules(account); + ? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus()) + : fetchPriceFloorData(account); } - private FetchResult fetchPriceFloorRules(Account account) { + private FetchResult fetchPriceFloorData(Account account) { final AccountPriceFloorsFetchConfig fetchConfig = getFetchConfig(account); final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled); @@ -109,7 +104,7 @@ private FetchResult fetchPriceFloorRules(Account account) { return FetchResult.of(null, FetchStatus.error); } if (!fetchInProgress.contains(accountId)) { - fetchPriceFloorRulesAsynchronous(fetchConfig, accountId); + fetchPriceFloorDataAsynchronous(fetchConfig, accountId); } return FetchResult.of(null, FetchStatus.inprogress); @@ -136,7 +131,7 @@ private static AccountPriceFloorsFetchConfig getFetchConfig(Account account) { return ObjectUtil.getIfNotNull(priceFloorsConfig, AccountPriceFloorsConfig::getFetch); } - private void fetchPriceFloorRulesAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) { + private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) { final Long accountTimeout = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getTimeout); final Long timeout = ObjectUtils.firstNonNull( ObjectUtil.getIfNotNull(debugProperties, PriceFloorDebugProperties::getMinTimeoutMs), @@ -151,7 +146,7 @@ private void fetchPriceFloorRulesAsynchronous(AccountPriceFloorsFetchConfig fetc .map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId)) .recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId)) .map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId)) - .map(priceFloorRules -> createPeriodicTimerForRulesFetch(priceFloorRules, fetchConfig, accountId)); + .map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId)); } private static long resolveMaxFileSize(Long maxSizeInKBytes) { @@ -174,60 +169,30 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon + "response body can not be empty", accountId)); } - final PriceFloorRules priceFloorRules = parsePriceFloorRules(body, accountId); + final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId); + PriceFloorRulesValidator.validateRulesData(priceFloorData, resolveMaxRules(fetchConfig.getMaxRules())); - validatePriceFloorRules(priceFloorRules, fetchConfig); - - return ResponseCacheInfo.of(priceFloorRules, + return ResponseCacheInfo.of(priceFloorData, FetchStatus.success, cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl())); } - private PriceFloorRules parsePriceFloorRules(String body, String accountId) { - final PriceFloorRules priceFloorRules; + private PriceFloorData parsePriceFloorData(String body, String accountId) { + final PriceFloorData priceFloorData; try { - priceFloorRules = mapper.decodeValue(body, PriceFloorRules.class); + priceFloorData = mapper.decodeValue(body, PriceFloorData.class); } catch (DecodeException e) { throw new PreBidException( String.format("Failed to parse price floor response for account %s, cause: %s", accountId, ExceptionUtils.getMessage(e))); } - return priceFloorRules; - } - - private void validatePriceFloorRules(PriceFloorRules priceFloorRules, AccountPriceFloorsFetchConfig fetchConfig) { - final PriceFloorData data = priceFloorRules.getData(); - - if (data == null) { - throw new PreBidException("Price floor rules data must be present"); - } - - if (CollectionUtils.isEmpty(data.getModelGroups())) { - throw new PreBidException("Price floor rules should contain at least one model group"); - } - - final int maxRules = resolveMaxRules(Math.toIntExact(fetchConfig.getMaxRules())); - - CollectionUtils.emptyIfNull(data.getModelGroups()).stream() - .filter(Objects::nonNull) - .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); - } - - private static int resolveMaxRules(Integer accountMaxRules) { - return Objects.equals(accountMaxRules, 0) ? Integer.MAX_VALUE : accountMaxRules; + return priceFloorData; } - private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) { - final Map values = modelGroup.getValues(); - if (MapUtils.isEmpty(values)) { - throw new PreBidException(String.format("Price floor rules values can't be null or empty, but were %s", - values)); - } - - if (values.size() > maxRules) { - throw new PreBidException(String.format("Price floor rules number %s exceeded its maximum number %s", - values.size(), maxRules)); - } + private static int resolveMaxRules(Long accountMaxRules) { + return accountMaxRules != null && !accountMaxRules.equals(0L) + ? Math.toIntExact(accountMaxRules) + : Integer.MAX_VALUE; } private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String fetchUrl) { @@ -249,20 +214,20 @@ private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String return null; } - private PriceFloorRules updateCache(ResponseCacheInfo cacheInfo, - AccountPriceFloorsFetchConfig fetchConfig, - String accountId) { + private PriceFloorData updateCache(ResponseCacheInfo cacheInfo, + AccountPriceFloorsFetchConfig fetchConfig, + String accountId) { long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig)); final AccountFetchContext fetchContext = - AccountFetchContext.of(cacheInfo.getRules(), cacheInfo.getFetchStatus(), maxAgeTimerId); + AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId); - if (cacheInfo.getRules() != null || !fetchedData.containsKey(accountId)) { + if (cacheInfo.getRulesData() != null || !fetchedData.containsKey(accountId)) { fetchedData.put(accountId, fetchContext); fetchInProgress.remove(accountId); } - return fetchContext.getRules(); + return fetchContext.getRulesData(); } private long resolveCacheTtl(ResponseCacheInfo cacheInfo, AccountPriceFloorsFetchConfig fetchConfig) { @@ -312,9 +277,9 @@ private Future recoverFromFailedFetching(Throwable throwable, return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus)); } - private PriceFloorRules createPeriodicTimerForRulesFetch(PriceFloorRules priceFloorRules, - AccountPriceFloorsFetchConfig fetchConfig, - String accountId) { + private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData, + AccountPriceFloorsFetchConfig fetchConfig, + String accountId) { final long accountPeriodicTimeSec = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec); final long periodicTimeSec = @@ -323,11 +288,11 @@ private PriceFloorRules createPeriodicTimerForRulesFetch(PriceFloorRules priceFl accountPeriodicTimeSec); vertx.setTimer(TimeUnit.SECONDS.toMillis(periodicTimeSec), ignored -> periodicFetch(accountId)); - return priceFloorRules; + return priceFloorData; } private void periodicFetch(String accountId) { - accountById(accountId).map(this::fetchPriceFloorRules); + accountById(accountId).map(this::fetchPriceFloorData); } private Future accountById(String accountId) { @@ -341,7 +306,7 @@ private Future accountById(String accountId) { @Value(staticConstructor = "of") private static class AccountFetchContext { - PriceFloorRules rules; + PriceFloorData rulesData; FetchStatus fetchStatus; @@ -351,7 +316,7 @@ private static class AccountFetchContext { @Value(staticConstructor = "of") private static class ResponseCacheInfo { - PriceFloorRules rules; + PriceFloorData rulesData; FetchStatus fetchStatus; diff --git a/src/main/java/org/prebid/server/floors/PriceFloorResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorResolver.java index 601d875343d..3b596027a9f 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorResolver.java @@ -3,8 +3,8 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; -import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; +import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.util.List; @@ -12,18 +12,18 @@ public interface PriceFloorResolver { PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, ImpMediaType mediaType, Format format, List warnings); default PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, List warnings) { - return resolve(bidRequest, modelGroup, imp, null, null, warnings); + return resolve(bidRequest, floorRules, imp, null, null, warnings); } static NoOpPriceFloorResolver noOp() { @@ -34,7 +34,7 @@ class NoOpPriceFloorResolver implements PriceFloorResolver { @Override public PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, ImpMediaType mediaType, Format format, diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java new file mode 100644 index 00000000000..086bf9f5919 --- /dev/null +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -0,0 +1,94 @@ +package org.prebid.server.floors; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorModelGroup; +import org.prebid.server.floors.model.PriceFloorRules; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Objects; + +public class PriceFloorRulesValidator { + + private static final int MODEL_WEIGHT_MAX_VALUE = 100; + private static final int MODEL_WEIGHT_MIN_VALUE = 1; + private static final int SKIP_RATE_MIN = 0; + private static final int SKIP_RATE_MAX = 100; + + private PriceFloorRulesValidator() { + } + + public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules) { + + final Integer rootSkipRate = priceFloorRules.getSkipRate(); + if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) { + throw new PreBidException(String.format("Price floor root skipRate " + + "must be in range(0-100), but was %s", rootSkipRate)); + } + + final BigDecimal floorMin = priceFloorRules.getFloorMin(); + if (floorMin != null && floorMin.compareTo(BigDecimal.ZERO) < 0) { + throw new PreBidException(String.format("Price floor floorMin " + + "must be positive float, but was %s", floorMin)); + } + + validateRulesData(priceFloorRules.getData(), maxRules); + } + + public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules) { + if (priceFloorData == null) { + throw new PreBidException("Price floor rules data must be present"); + } + + final Integer dataSkipRate = priceFloorData.getSkipRate(); + if (dataSkipRate != null && (dataSkipRate < SKIP_RATE_MIN || dataSkipRate > SKIP_RATE_MAX)) { + throw new PreBidException(String.format("Price floor data skipRate " + + "must be in range(0-100), but was %s", dataSkipRate)); + } + + if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) { + throw new PreBidException("Price floor rules should contain at least one model group"); + } + + priceFloorData.getModelGroups().stream() + .filter(Objects::nonNull) + .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); + } + + private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) { + final Integer modelWeight = modelGroup.getModelWeight(); + if (modelWeight != null + && (modelWeight < MODEL_WEIGHT_MIN_VALUE || modelWeight > MODEL_WEIGHT_MAX_VALUE)) { + + throw new PreBidException(String.format("Price floor modelGroup modelWeight " + + "must be in range(1-100), but was %s", modelWeight)); + + } + + final Integer skipRate = modelGroup.getSkipRate(); + if (skipRate != null && (skipRate < SKIP_RATE_MIN || skipRate > SKIP_RATE_MAX)) { + throw new PreBidException(String.format("Price floor modelGroup skipRate " + + "must be in range(0-100), but was %s", skipRate)); + } + + final BigDecimal defaultPrice = modelGroup.getDefaultFloor(); + if (defaultPrice != null && defaultPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new PreBidException(String.format("Price floor modelGroup default " + + "must be positive float, but was %s", defaultPrice)); + } + + final Map values = modelGroup.getValues(); + if (MapUtils.isEmpty(values)) { + throw new PreBidException(String.format("Price floor rules values can't be null or empty, but were %s", + values)); + } + + if (maxRules != null && values.size() > maxRules) { + throw new PreBidException(String.format("Price floor rules number %s exceeded its maximum number %s", + values.size(), maxRules)); + } + } +} diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java b/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java index 993de9c39e1..ccc5e05bb5e 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java @@ -7,22 +7,10 @@ import java.math.BigDecimal; -/** - * This model is a trade-off. - *

- * It defines both: - * 1. The contract for prebid server bidrequest.ext.prebid.floors field. - * 2. The contract for floors provider (assuming prebid server specific fields will not be overridden). - *

- * To make things better, it should be divided in two separate models: - * for prebid request and floors provider. - */ @Value @Builder(toBuilder = true) public class PriceFloorRules { - // prebid server and floors provider fields - @JsonProperty("floorMin") BigDecimal floorMin; @@ -38,8 +26,6 @@ public class PriceFloorRules { PriceFloorData data; - // prebid server specific fields - Boolean enabled; @JsonProperty("fetchStatus") diff --git a/src/main/java/org/prebid/server/floors/proto/FetchResult.java b/src/main/java/org/prebid/server/floors/proto/FetchResult.java index 774f7e23932..36c4fda58e0 100644 --- a/src/main/java/org/prebid/server/floors/proto/FetchResult.java +++ b/src/main/java/org/prebid/server/floors/proto/FetchResult.java @@ -1,12 +1,12 @@ package org.prebid.server.floors.proto; import lombok.Value; -import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.floors.model.PriceFloorData; @Value(staticConstructor = "of") public class FetchResult { - PriceFloorRules rules; + PriceFloorData rulesData; FetchStatus fetchStatus; } diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 6647b2ef09f..fc4fb11a9a2 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -17,16 +17,19 @@ public class EnrichingApplicationSettings implements ApplicationSettings { + private final boolean enforceValidAccount; private final ApplicationSettings delegate; private final PriceFloorsConfigResolver priceFloorsConfigResolver; private final JsonMerger jsonMerger; private final Account defaultAccount; - public EnrichingApplicationSettings(String defaultAccountConfig, + public EnrichingApplicationSettings(boolean enforceValidAccount, + String defaultAccountConfig, ApplicationSettings delegate, PriceFloorsConfigResolver priceFloorsConfigResolver, JsonMerger jsonMerger) { + this.enforceValidAccount = enforceValidAccount; this.delegate = Objects.requireNonNull(delegate); this.jsonMerger = Objects.requireNonNull(jsonMerger); this.priceFloorsConfigResolver = Objects.requireNonNull(priceFloorsConfigResolver); @@ -42,10 +45,13 @@ public Future getAccountById(String accountId, Timeout timeout) { if (defaultAccount == null) { return accountFuture; } + final Future mergedWithDefaultAccount = accountFuture + .map(this::mergeAccounts); - return accountFuture - .map(this::mergeAccounts) - .otherwise(mergeAccounts(Account.empty(accountId))); + // In case of invalid account return failed future + return enforceValidAccount + ? mergedWithDefaultAccount + : mergedWithDefaultAccount.otherwise(mergeAccounts(Account.empty(accountId))); } @Override diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java index 4e881101264..20f9a9f81cf 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -79,10 +79,9 @@ PriceFloorResolver noOpPriceFloorResolver() { @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") PriceFloorProcessor basicPriceFloorProcessor(PriceFloorFetcher floorFetcher, PriceFloorResolver floorResolver, - CurrencyConversionService conversionService, JacksonMapper mapper) { - return new BasicPriceFloorProcessor(floorFetcher, floorResolver, conversionService, mapper); + return new BasicPriceFloorProcessor(floorFetcher, floorResolver, mapper); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 72ceea87e01..7ef4ae5fbdd 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -340,12 +340,14 @@ static class EnrichingSettingsConfiguration { @Bean EnrichingApplicationSettings enrichingApplicationSettings( + @Value("${settings.enforce-valid-account}") boolean enforceValidAccount, @Value("${settings.default-account-config:#{null}}") String defaultAccountConfig, CompositeApplicationSettings compositeApplicationSettings, PriceFloorsConfigResolver priceFloorsConfigResolver, JsonMerger jsonMerger) { - return new EnrichingApplicationSettings(defaultAccountConfig, + return new EnrichingApplicationSettings(enforceValidAccount, + defaultAccountConfig, compositeApplicationSettings, priceFloorsConfigResolver, jsonMerger); diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java new file mode 100644 index 00000000000..4a232c39bec --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java @@ -0,0 +1,47 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.GenericBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.util.HttpUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/aax.yaml", factory = YamlPropertySourceFactory.class) +public class AaxConfiguration { + + private static final String BIDDER_NAME = "aax"; + private static final String EXTERNAL_URL_MACRO = "{{PREBID_SERVER_ENDPOINT}}"; + + @Bean("aaxConfigurationProperties") + @ConfigurationProperties("adapters.aax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps aaxBidderDeps(BidderConfigurationProperties aaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(aaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new GenericBidder(resolveEndpoint(config.getEndpoint(), externalUrl), mapper)) + .assemble(); + } + + private String resolveEndpoint(String configEndpoint, String externalUrl) { + return configEndpoint.replace(EXTERNAL_URL_MACRO, HttpUtil.encodeUrl(externalUrl)); + } +} diff --git a/src/main/resources/bidder-config/aax.yaml b/src/main/resources/bidder-config/aax.yaml new file mode 100644 index 00000000000..e6bda296d94 --- /dev/null +++ b/src/main/resources/bidder-config/aax.yaml @@ -0,0 +1,21 @@ +adapters: + aax: + endpoint: https://prebid.aaxads.com/rtb/pb/aax-prebid?src={{PREBID_SERVER_ENDPOINT}} + meta-info: + maintainer-email: product@aax.media + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 720 + usersync: + url: https://c.aaxads.com/aacxc.php?fv=1&wbsh=psa&ryvlg=setstatuscode&redirect= + redirect-url: /setuid?bidder=aax&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid= + cookie-family-name: aax + type: redirect + support-cors: false diff --git a/src/main/resources/static/bidder-params/aax.json b/src/main/resources/static/bidder-params/aax.json new file mode 100644 index 00000000000..83cdfc59406 --- /dev/null +++ b/src/main/resources/static/bidder-params/aax.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Aax Adapter Params", + "description": "A schema which validates params accepted by the Aax adapter", + "type": "object", + "properties": { + "cid": { + "type": "string", + "description": "The customer id provided by AAX." + }, + "crid": { + "type": "string", + "description": "The placement id provided by AAX." + } + }, + "required": [ + "cid", + "crid" + ] +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy index f344c203618..3d88cb51248 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy @@ -18,4 +18,8 @@ class AccountConfig { AccountAnalyticsConfig analytics AccountCookieSyncConfig cookieSync AccountHooksConfiguration hooks + + static getDefaultAccountConfig() { + new AccountConfig(status: AccountStatus.ACTIVE) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy deleted file mode 100644 index 6f99c65502a..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.functional.model.mock.services.floorsprovider - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class PriceFloorEndpoint { - - String url -} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy deleted file mode 100644 index ff3af553ded..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy +++ /dev/null @@ -1,26 +0,0 @@ -package org.prebid.server.functional.model.mock.services.floorsprovider - -import groovy.transform.ToString -import org.prebid.server.functional.model.ResponseModel -import org.prebid.server.functional.model.pricefloors.PriceFloorData -import org.prebid.server.functional.model.pricefloors.PriceFloorEnforcement -import org.prebid.server.functional.util.PBSUtils - -import static org.prebid.server.functional.tests.pricefloors.PriceFloorsBaseSpec.FLOOR_MIN - -@ToString(includeNames = true, ignoreNulls = true) -class PriceFloorRules implements ResponseModel { - - BigDecimal floorMin - String floorProvider - PriceFloorEnforcement enforcement - Integer skipRate - PriceFloorEndpoint endpoint - PriceFloorData data - - static PriceFloorRules getPriceFloorRules() { - new PriceFloorRules(floorMin: FLOOR_MIN, - floorProvider: PBSUtils.randomString, - data: PriceFloorData.priceFloorData) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy index 4498bb3e30d..b9af0cd1dc7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy @@ -3,13 +3,14 @@ package org.prebid.server.functional.model.pricefloors import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.ResponseModel import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.USD @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) -class PriceFloorData { +class PriceFloorData implements ResponseModel { String floorProvider Currency currency diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index acae8b3e586..4ce40176d72 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -44,7 +44,7 @@ class Imp { } } - private static Imp getDefaultImp() { + private static Imp getDefaultImp() { new Imp().tap { id = UUID.randomUUID() ext = ImpExt.defaultImpExt diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index de67c5b3664..e009b5a1741 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -32,6 +32,6 @@ class Video { List companiontype static Video getDefaultVideo() { - new Video(mimes: ["video/mp4"], w: 300, h: 200) + new Video(mimes: ["video/mp4"], w: 300, h: 200) } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy index 935a20b80a7..00bbf9c3fe4 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy @@ -4,7 +4,7 @@ import org.mockserver.matchers.TimeToLive import org.mockserver.matchers.Times import org.mockserver.model.HttpRequest import org.mockserver.model.HttpResponse -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.util.ObjectMapperWrapper import org.testcontainers.containers.MockServerContainer @@ -39,6 +39,6 @@ class FloorsProvider extends NetworkScaffolding { } private String getDefaultResponse() { - mapper.encode(PriceFloorRules.priceFloorRules) + mapper.encode(PriceFloorData.priceFloorData) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy new file mode 100644 index 00000000000..925a2f78794 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy @@ -0,0 +1,256 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.AccountStatus +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.util.PBSUtils + +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED + +class AccountSpec extends BaseSpec { + + def "PBS should reject request with inactive account"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account": enforceValidAccount as String]) + + and: "Inactive account id" + def accountId = PBSUtils.randomNumber + def account = new Account(uuid: accountId, config: new AccountConfig(status: AccountStatus.INACTIVE)) + accountDao.save(account) + + and: "Default basic BidRequest with inactive account id" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = accountId + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Account $accountId is inactive" + + where: + enforceValidAccount << [true, false] + } + + def "PBS should reject request with unknown account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Non-existing account id" + def accountId = PBSUtils.randomNumber + + and: "Default basic BidRequest with non-existing account id" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = accountId + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: $accountId" + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } + + def "PBS should reject request without account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default basic BidRequest without account" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = null + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: " + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } + + def "PBS should not reject request with unknown account when settings.enforce-valid-account = false"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "false", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default basic BidRequest with non-existing account id" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = accountId + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + defaultAccountConfig || accountId + null || null + null || PBSUtils.randomNumber + AccountConfig.defaultAccountConfig || null + AccountConfig.defaultAccountConfig || PBSUtils.randomNumber + } + + def "PBS AMP should reject request with unknown account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request with non-existing account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = requestAccount + } + + and: "Default stored request with non-existing account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = storedRequestAccount + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + pbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + def resolvedAccount = requestAccount ?: storedRequestAccount + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: $resolvedAccount" + + where: + defaultAccountConfig || requestAccount || storedRequestAccount + null || PBSUtils.randomNumber || null + null || null || PBSUtils.randomNumber + AccountConfig.defaultAccountConfig || PBSUtils.randomNumber || null + AccountConfig.defaultAccountConfig || null || PBSUtils.randomNumber + } + + def "PBS AMP should reject request without account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request without account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = null + } + + and: "Default stored request without account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = null + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + pbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: " + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } + + def "PBS AMP should not reject request with unknown account when settings.enforce-valid-account = false"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "false", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request with non-existing account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = requestAccount + } + + and: "Default stored request with non-existing account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = storedRequestAccount + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = pbsService.sendAmpRequest(ampRequest) + + then: "PBS should not reject request" + assert response.targeting + assert response.ext?.debug?.httpcalls + + where: + defaultAccountConfig || requestAccount || storedRequestAccount + null || PBSUtils.randomNumber || null + null || null || PBSUtils.randomNumber + AccountConfig.defaultAccountConfig || PBSUtils.randomNumber || null + AccountConfig.defaultAccountConfig || null || PBSUtils.randomNumber + } + + def "PBS AMP should not reject request without account when settings.enforce-valid-account = false"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "false", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request without account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = null + } + + and: "Default stored request without account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = null + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = pbsService.sendAmpRequest(ampRequest) + + then: "PBS should not reject request" + assert response.targeting + assert response.ext?.debug?.httpcalls + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index f80a88d7183..239e6a690e3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -54,8 +54,8 @@ abstract class BaseSpec extends Specification { PBSUtils.getRandomNumber(MIN_TIMEOUT, MAX_TIMEOUT) } - protected static Number getCurrentMetricValue(String name) { - def response = defaultPbsService.sendCollectedMetricsRequest() + protected static Number getCurrentMetricValue(PrebidServerService pbsService = defaultPbsService, String name) { + def response = pbsService.sendCollectedMetricsRequest() response[name] ?: 0 } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index edc6953fbfd..14d5058a688 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -26,6 +26,7 @@ import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS @PBSTest abstract class PriceFloorsBaseSpec extends BaseSpec { @@ -39,8 +40,8 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { FloorsProvider.FLOORS_ENDPOINT protected static final FloorsProvider floorsProvider = new FloorsProvider(Dependencies.networkServiceContainer, Dependencies.objectMapperWrapper) + protected static final int MAX_MODEL_WEIGHT = 100 private static final int DEFAULT_MODEL_WEIGHT = 1 - private static final int MAX_MODEL_WEIGHT = 1000000 private static final int CURRENCY_CONVERSION_PRECISION = 3 private static final int FLOOR_VALUE_PRECISION = 4 @@ -113,8 +114,9 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { } protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, BidRequest bidRequest) { - pbsService.sendAuctionRequest(bidRequest) - Thread.sleep(1000) + PBSUtils.waitUntil({ pbsService.sendAuctionRequest(bidRequest).ext?.debug?.resolvedRequest?.ext?.prebid?.floors?.fetchStatus != INPROGRESS }, + 5000, + 1000) } protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index e9e4a52165e..be81051b597 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -1,7 +1,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse @@ -33,10 +33,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -50,7 +49,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency } } @@ -66,10 +65,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the request.cur" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -78,7 +76,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Bid response with 2 bids: price < floorMin, price = floorMin" def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorValue, - floorsResponse.data.modelGroups[0].currency, bidRequest.cur[0]) + floorsResponse.modelGroups[0].currency, bidRequest.cur[0]) def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = EUR seatbid.first().bid << Bid.getDefaultBid(bidRequest.imp.first()) @@ -116,9 +114,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorMin, bidRequest.ext.prebid.floors.floorMinCur, floorProviderCur) - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): convertedMinFloorValue - 0.1] - data.modelGroups[0].currency = floorProviderCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): convertedMinFloorValue - 0.1] + modelGroups[0].currency = floorProviderCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -160,15 +158,18 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = EUR - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] - data.modelGroups[0].currency = floorsProviderCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] + modelGroups[0].currency = floorsProviderCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(pbsService, bidRequest) + and: "Flush metrics" + flushMetrics(pbsService) + when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -176,7 +177,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { assert response.ext?.errors[ErrorType.GENERIC]*.code == [999] assert response.ext?.errors[ErrorType.GENERIC]*.message == ["Unable to convert from currency $bidRequest.ext.prebid.floors.floorMinCur to desired ad server" + - " currency ${floorsResponse.data.modelGroups[0].currency}" as String] + " currency ${floorsResponse.modelGroups[0].currency}" as String] and: "PBS should log a warning" assert response.ext?.warnings[PREBID]*.code == [999] @@ -185,8 +186,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { "to convert from currency $requestFloorCur to desired ad server currency $floorsProviderCur" as String] and: "Metric #GENERAL_ERROR_METRIC should be update" - def metrics = pbsService.sendCollectedMetricsRequest() - assert metrics[GENERAL_ERROR_METRIC] == 1 + assert getCurrentMetricValue(pbsService, GENERAL_ERROR_METRIC) == 1 and: "Bidder request should contain bidFloor, bidFloorCur from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -254,10 +254,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the request.cur" def floorValue = PBSUtils.randomFloorValue def floorCur = USD - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = floorCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = floorCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -310,15 +309,18 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = BOGUS - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] - data.modelGroups[0].currency = floorsProviderCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] + modelGroups[0].currency = floorsProviderCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) + and: "Flush metrics" + flushMetrics(floorsPbsService) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -329,8 +331,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { "to convert from currency $requestFloorCur to desired ad server currency $floorsProviderCur" as String] and: "Metric #GENERAL_ERROR_METRIC should be update" - def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[GENERAL_ERROR_METRIC] == 1 + assert getCurrentMetricValue(floorsPbsService, GENERAL_ERROR_METRIC) == 1 and: "Bidder request should contain bidFloor, bidFloorCur from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy index f5c5a1ff711..bf6e6624de5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy @@ -3,8 +3,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules -import org.prebid.server.functional.model.pricefloors.PriceFloorEnforcement +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors import org.prebid.server.functional.model.request.auction.BidRequest @@ -46,8 +45,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(ampRequest.account as String, floorsResponse) @@ -79,7 +78,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { } and: "PBS should log warning about bid suppression" - assert response.ext?.warnings[ErrorType.GENERIC_ALIAS]*.code == [999] + assert response.ext?.warnings[ErrorType.GENERIC_ALIAS]*.code == [6] assert response.ext?.warnings[ErrorType.GENERIC_ALIAS]*.message == ["Bid with id '${aliasBidResponse.seatbid[0].bid[0].id}' was rejected by floor enforcement: " + "price $lowerPrice is below the floor $floorValue" as String] @@ -103,8 +102,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -128,7 +127,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { assert response.seatbid?.first()?.bid?.collect { it.price } == [floorValue] and: "PBS should log warning about suppression all bids below the floor value " - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [999, 999] + assert response.ext?.warnings[ErrorType.GENERIC]*.code == [6, 6] assert response.ext?.warnings[ErrorType.GENERIC]*.message == ["Bid with id '${bidResponse.seatbid[0].bid[1].id}' was rejected by floor enforcement: " + "price ${bidResponse.seatbid[0].bid[1].price} is below the floor $floorValue" as String, @@ -143,6 +142,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { given: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] + ext.prebid.floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(enforcePbs: false)) } and: "Account with enabled fetch, fetch.url in the DB" @@ -151,9 +151,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(enforcePbs: false) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -231,6 +230,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Default basic BidRequest with generic bidder with preferdeals = true" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(preferdeals: true) + ext.prebid.floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(floorDeals: true)) } and: "Account with enabled fetch, fetch.url,enforceDealFloors in the DB" @@ -241,9 +241,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: true) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -280,6 +279,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Default basic BidRequest with generic bidder with preferdeals = true" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(preferdeals: true) + ext.prebid.floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(floorDeals: floorDeals, enforcePbs: enforcePbs)) } and: "Account with enabled fetch, fetch.url, enforceDealFloors in the DB" @@ -290,9 +290,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: floorDeals, enforcePbs: enforcePbs) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -347,9 +346,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: true) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -403,9 +401,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: true) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -452,8 +449,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 9781999348f..3eed5329d8c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -2,8 +2,8 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest @@ -15,23 +15,29 @@ import org.prebid.server.functional.util.PBSUtils import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 -import static org.prebid.server.functional.model.Currency.USD +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.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH import static org.prebid.server.functional.model.request.auction.Location.REQUEST +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { - private static final int maxEnforceFloorsRate = 100 - + private static final int MAX_ENFORCE_FLOORS_RATE = 100 private static final int DEFAULT_MAX_AGE_SEC = 600 private static final int DEFAULT_PERIOD_SEC = 300 private static final int MIN_TIMEOUT_MS = 10 private static final int MAX_TIMEOUT_MS = 10000 + private static final int MIN_SKIP_RATE = 0 + private static final int MAX_SKIP_RATE = 100 + private static final int MIN_DEFAULT_FLOOR_VALUE = 0 + private static final int MIN_FLOOR_MIN = 0 private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure" @@ -51,7 +57,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { cacheFloorsProviderRules(pbsService, bidRequest) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data" assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 @@ -256,7 +262,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert !response.seatbid?.isEmpty() where: - enforceFloorsRate << [PBSUtils.randomNegativeNumber, maxEnforceFloorsRate + 1] + enforceFloorsRate << [PBSUtils.randomNegativeNumber, MAX_ENFORCE_FLOORS_RATE + 1] } def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() { @@ -330,8 +336,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -527,8 +533,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response without modelGroups" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups = null } floorsProvider.setResponse(accountId, floorsResponse) @@ -569,8 +575,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response without rules" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = null } floorsProvider.setResponse(accountId, floorsResponse) @@ -614,8 +620,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response with 2 rules" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values.put(new Rule(mediaType: BANNER, country: MULTIPLE).rule, 0.7) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values.put(new Rule(mediaType: BANNER, country: MULTIPLE).rule, 0.7) } floorsProvider.setResponse(accountId, floorsResponse) @@ -694,14 +700,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Account with maxFileSizeKb in the DB" def accountId = bidRequest.app.publisher.id - def maxSize = PBSUtils.getRandomNumber(0, 10) + def maxSize = PBSUtils.getRandomNumber(1, 10) def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.fetch.maxFileSizeKb = maxSize } accountDao.save(account) and: "Set Floors Provider response with Content-Length" - def floorsResponse = PriceFloorRules.priceFloorRules + def floorsResponse = PriceFloorData.priceFloorData def responseSize = convertKilobyteSizeToByte(maxSize) + 100 floorsProvider.setResponse(accountId, floorsResponse, ["Content-Length": responseSize as String]) @@ -764,7 +770,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == storedRequestModel.ext.prebid.floors.floorMin - ext?.prebid?.floors?.floorProvider == storedRequestModel.ext.prebid.floors.floorProvider + ext?.prebid?.floors?.floorProvider == storedRequestModel.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == storedRequestModel.ext.prebid.floors.data } @@ -807,7 +813,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == bidRequest.ext.prebid.floors.floorMin - ext?.prebid?.floors?.floorProvider == bidRequest.ext.prebid.floors.floorProvider + ext?.prebid?.floors?.floorProvider == bidRequest.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == bidRequest.ext.prebid.floors.data } } @@ -846,7 +852,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == ampStoredRequest.ext.prebid.floors.floorMin - ext?.prebid?.floors?.floorProvider == ampStoredRequest.ext.prebid.floors.floorProvider + ext?.prebid?.floors?.floorProvider == ampStoredRequest.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == ampStoredRequest.ext.prebid.floors.data } } @@ -855,6 +861,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { given: "BidRequest with storedRequest" def bidRequest = bidRequestWithFloors.tap { ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.floors.floorMin = FLOOR_MIN } and: "Default stored request with floors" @@ -870,8 +877,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -882,19 +889,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin + ext?.prebid?.floors?.floorMin == bidRequest.ext.prebid.floors.floorMin ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse } } @@ -903,7 +910,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def ampRequest = AmpRequest.defaultAmpRequest and: "Default stored request with floors " - def ampStoredRequest = storedRequestWithFloors + def ampStoredRequest = storedRequestWithFloors.tap { + ext.prebid.floors.floorMin = FLOOR_MIN + } def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -913,8 +922,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(ampRequest.account as String, floorsResponse) @@ -925,19 +934,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last() verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin + ext?.prebid?.floors?.floorMin == ampStoredRequest.ext.prebid.floors.floorMin ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse } } @@ -968,8 +977,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { where: description | floorsResponse - "valid" | PriceFloorRules.priceFloorRules - "invalid" | PriceFloorRules.priceFloorRules.tap { data.modelGroups = null } + "valid" | PriceFloorData.priceFloorData + "invalid" | PriceFloorData.priceFloorData.tap { modelGroups = null } } def "PBS should continue to hold onto previously fetched rules when fetch.enabled = false in account config"() { @@ -989,8 +998,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider #description response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -1012,29 +1021,115 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin + !ext?.prebid?.floors?.floorMin ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse + } + } + + def "PBS should validate rules from request when floorMin from request is invalid"() { + given: "Default BidRequest with floorMin" + def floorValue = PBSUtils.randomFloorValue + def invalidFloorMin = MIN_FLOOR_MIN - 1 + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.floorMin = invalidFloorMin + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor floorMin " + + "must be positive float, but was $invalidFloorMin "] + } + + def "PBS should validate rules from request when request doesn't contain modelGroups"() { + given: "Default BidRequest without modelGroups" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups = null + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor rules " + + "should contain at least one model group "] + } + + def "PBS should validate rules from request when request doesn't contain values"() { + given: "Default BidRequest without rules" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups[0].values = null + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor rules values " + + "can't be null or empty, but were null "] } def "PBS should validate rules from request when modelWeight from request is invalid"() { given: "Default BidRequest with floors" def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight - ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue] + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] ext.prebid.floors.data.modelGroups.last().modelWeight = modelWeight } @@ -1045,14 +1140,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + def response = floorsPbsService.sendAuctionRequest(bidRequest) - then: "Bidder request bidFloor should correspond to valid modelGroup" + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == floorValue + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " + + "must be in range(1-100), but was $invalidModelWeight "] where: - invalidModelWeight << [0, -1, 1000000] + invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] } def "PBS should validate rules from amp request when modelWeight from request is invalid"() { @@ -1062,10 +1162,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Default stored request with floors" def floorValue = PBSUtils.randomFloorValue def ampStoredRequest = storedRequestWithFloors.tap { + imp[0].bidFloor = floorValue ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight - ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue] + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] ext.prebid.floors.data.modelGroups.last().modelWeight = modelWeight } def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) @@ -1078,14 +1179,173 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) when: "PBS processes auction request" - floorsPbsService.sendAmpRequest(ampRequest) + def response = floorsPbsService.sendAmpRequest(ampRequest) then: "Bidder request bidFloor should correspond to valid modelGroup" - def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " + + "must be in range(1-100), but was $invalidModelWeight "] + + where: + invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] + } + + def "PBS should reject fetch when root skipRate from request is invalid"() { + given: "Default BidRequest with skipRate" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].skipRate = 0 + ext.prebid.floors.data.skipRate = 0 + ext.prebid.floors.skipRate = invalidSkipRate + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().skipRate = 0 + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor root skipRate " + + "must be in range(0-100), but was $invalidSkipRate "] + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when data skipRate from request is invalid"() { + given: "Default BidRequest with skipRate" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].skipRate = 0 + ext.prebid.floors.data.skipRate = invalidSkipRate + ext.prebid.floors.skipRate = 0 + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().skipRate = 0 + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor data skipRate " + + "must be in range(0-100), but was $invalidSkipRate "] + where: - invalidModelWeight << [0, -1, 1000000] + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when modelGroup skipRate from request is invalid"() { + given: "Default BidRequest with skipRate" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].skipRate = invalidSkipRate + ext.prebid.floors.data.skipRate = 0 + ext.prebid.floors.skipRate = 0 + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().skipRate = 0 + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup skipRate " + + "must be in range(0-100), but was $invalidSkipRate "] + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should validate rules from request when default floor value from request is invalid"() { + given: "Default BidRequest with default floor value" + def floorValue = PBSUtils.randomFloorValue + def invalidDefaultFloorValue = MIN_DEFAULT_FLOOR_VALUE - 1 + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup default " + + "must be positive float, but was $invalidDefaultFloorValue "] } def "PBS should not invalidate previously good fetched data when floors provider return invalid data"() { @@ -1104,8 +1364,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider #description response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -1124,40 +1384,103 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin + !ext?.prebid?.floors?.floorMin ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse } } - def "PBS should prefer floorMin from request over floorMin from fetched data"() { - given: "Default BidRequest" - def floorMin = PBSUtils.randomFloorValue - def floorMinCur = USD - def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - ext.prebid.floors = new ExtPrebidFloors(floorMin: floorMin, floorMinCur: floorMinCur) + def "PBS should reject fetch when modelWeight from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, fetch.url in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups.first().modelWeight = invalidModelWeight + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().modelWeight = modelWeight } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" + + " must be in range(1-100), but was $invalidModelWeight") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] + } + + def "PBS should reject fetch when data skipRate from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.site.publisher.id def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) - and: "Set Floors Provider #description response" - def floorValue = floorMin - 0.1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = floorMinCur - it.floorMin = floorValue + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups[0].skipRate = 0 + skipRate = invalidSkipRate + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().skipRate = 0 } floorsProvider.setResponse(accountId, floorsResponse) @@ -1165,63 +1488,163 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { cacheFloorsProviderRules(bidRequest) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + def response = floorsPbsService.sendAuctionRequest(bidRequest) - then: "Bidder request floorMin should correspond to floorMin from request" - assert bidder.getRequestCount(bidRequest.id) == 2 + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert bidderRequest.ext?.prebid?.floors?.floorMin == floorMin + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor data skipRate" + + " must be in range(0-100), but was $invalidSkipRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] } - def "PBS should reject entire ruleset when modelWeight from floors provider is invalid"() { - given: "Default BidRequest" + def "PBS should reject fetch when modelGroup skipRate from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups.first().modelWeight = invalidModelWeight - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().modelWeight = modelWeight + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups[0].skipRate = invalidSkipRate + skipRate = 0 + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().skipRate = 0 } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() - then: "Bidder request bidFloor should correspond to rule from valid modelGroup" + then: "Bidder request bidFloor should not be passed" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert bidderRequest.imp[0].bidFloor == floorValue + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + + " must be in range(0-100), but was $invalidSkipRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() where: - invalidModelWeight << [0, -1, 1000000] + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] } - def "PBS should reject entire ruleset when skipRate from floors provider is invalid"() { - given: "Default BidRequest" + def "PBS should reject fetch when default floor value from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest + and: "Account with enabled fetch, fetch.url in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1 + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups[0].defaultFloor = invalidDefaultFloor + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup default" + + " must be positive float, but was $invalidDefaultFloor") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + } + + def "PBS should give preference to currency from modelGroups when signalling"() { + given: "Default BidRequest with floors" + def bidRequest = bidRequestWithFloors + and: "Account with enabled fetch, fetch.url in the DB" def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups.first().skipRate = invalidSkipRate - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().skipRate = 0 + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = modelGroupCurrency + currency = dataCurrency } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -1231,12 +1654,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { when: "PBS processes auction request" floorsPbsService.sendAuctionRequest(bidRequest) - then: "Bidder request bidFloor should correspond to rule from valid modelGroup" + then: "Bidder request should contain bidFloorCur from floors provider according to priority" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert bidderRequest.imp[0].bidFloor == floorValue + assert bidderRequest.imp[0].bidFloorCur == JPY where: - invalidSkipRate << [-1, 101] + modelGroupCurrency | dataCurrency + JPY | EUR + null | JPY } static int convertKilobyteSizeToByte(int kilobyteSize) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index 6521cda5a57..1c6d149b824 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -1,9 +1,10 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules + import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.auction.App @@ -56,9 +57,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { def floorValue = 0.8 def invalidRule = new Rule(mediaType: BANNER, country: Country.MULTIPLE, siteDomain: PBSUtils.randomString).rule - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY]) - data.modelGroups[0].values = [(rule) : floorValue, + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY]) + modelGroups[0].values = [(rule) : floorValue, (invalidRule): floorValue + 0.1] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -86,9 +87,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY], delimiter: delimiter) - data.modelGroups[0].values = [(new Rule(delimiter: delimiter, mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE).rule) : PBSUtils.randomFloorValue, + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY], delimiter: delimiter) + modelGroups[0].values = [(new Rule(delimiter: delimiter, mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE).rule) : PBSUtils.randomFloorValue, (new Rule(delimiter: delimiter, mediaType: BANNER, country: Country.MULTIPLE).rule): floorValue]} floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -133,9 +134,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[0].values = [(new Rule(domain: domain).rule.toUpperCase()) : floorValue, (new Rule(domain: PBSUtils.randomString).rule.toUpperCase()): floorValue + 0.1] } @@ -166,12 +167,12 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups[0].schema = new PriceFloorSchema(fields: [BOGUS]) - data.modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue + 0.1] - data.modelGroups[1].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[1].values = [(new Rule(domain: domain).rule) : floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups[0].schema = new PriceFloorSchema(fields: [BOGUS]) + modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue + 0.1] + modelGroups[1].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[1].values = [(new Rule(domain: domain).rule) : floorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -215,9 +216,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.getRoundedFractionalNumber(PBSUtils.getFractionalRandomNumber(FLOOR_MIN, 2), 6) as BigDecimal - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -238,9 +239,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) + modelGroups[0].values = [(new Rule(mediaType: MediaType.MULTIPLE).rule): bothFloorValue, (new Rule(mediaType: BANNER).rule) : bannerFloorValue, (new Rule(mediaType: VIDEO).rule) : videoFloorValue] @@ -282,9 +283,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def requestFloorValue = 0.8 def floorsProviderFloorValue = requestFloorValue + 0.1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) + modelGroups[0].values = [(new Rule(size: "*").rule) : floorsProviderFloorValue, (new Rule(size: "${lowerWidth}x${lowerHigh}").rule) : floorsProviderFloorValue + 0.1, (new Rule(size: "${higherWidth}x${higherHigh}").rule): floorsProviderFloorValue + 0.2] @@ -315,9 +316,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def requestFloorValue = 0.8 def floorsProviderFloorValue = requestFloorValue + 0.1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) + modelGroups[0].values = [(new Rule(size: "*").rule) : floorsProviderFloorValue + 0.1, (new Rule(size: "${width}x${height}").rule): floorsProviderFloorValue] } @@ -358,9 +359,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue, (new Rule(domain: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -407,9 +408,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) + modelGroups[0].values = [(new Rule(siteDomain: domain).rule) : floorValue, (new Rule(siteDomain: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -448,9 +449,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [PUB_DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [PUB_DOMAIN]) + modelGroups[0].values = [(new Rule(pubDomain: domain).rule) : floorValue, (new Rule(pubDomain: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -490,9 +491,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [BUNDLE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [BUNDLE]) + modelGroups[0].values = [(new Rule(bundle: bundle).rule) : floorValue, (new Rule(bundle: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -522,9 +523,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [CHANNEL]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [CHANNEL]) + modelGroups[0].values = [(new Rule(channel: channel).rule) : floorValue, (new Rule(channel: APP).rule): floorValue + 0.1] } @@ -552,9 +553,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [GPT_SLOT]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [GPT_SLOT]) + modelGroups[0].values = [(new Rule(gptSlot: gptSlot).rule) : floorValue, (new Rule(gptSlot: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -590,9 +591,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [PB_AD_SLOT]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [PB_AD_SLOT]) + modelGroups[0].values = [(new Rule(pbAdSlot: pbAdSlot).rule) : floorValue, (new Rule(pbAdSlot: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -622,9 +623,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [COUNTRY]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [COUNTRY]) + modelGroups[0].values = [(new Rule(country: country).rule) : floorValue, (new Rule(country: Country.MULTIPLE).rule): floorValue + 0.1] } @@ -652,9 +653,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) + modelGroups[0].values = [(new Rule(deviceType: PHONE).rule): phoneFloorValue, (new Rule(deviceType: TABLET).rule): tabletFloorValue, (new Rule(deviceType: DESKTOP).rule): desktopFloorValue, @@ -698,9 +699,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with wildcard deviceType rule" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) + modelGroups[0].values = [(new Rule(deviceType: PHONE).rule): floorValue + 0.1, (new Rule(deviceType: TABLET).rule): floorValue + 0.2, (new Rule(deviceType: DESKTOP).rule): floorValue + 0.3, @@ -729,10 +730,10 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) - data.modelGroups[0].values = [(new Rule(mediaType: VIDEO).rule): floorValue + 0.1] - data.modelGroups[0].defaultFloor = floorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) + modelGroups[0].values = [(new Rule(mediaType: VIDEO).rule): floorValue + 0.1] + modelGroups[0].defaultFloor = floorValue } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy index 5436daa9245..36dd199a9e3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy @@ -1,9 +1,9 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.amp.AmpRequest @@ -32,11 +32,13 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def "PBS should skip signalling for request with rules when ext.prebid.floors.enabled = false in request"() { given: "Default BidRequest with disabled floors" def bidRequest = bidRequestWithFloors.tap { - ext.prebid.floors.enabled = false + ext.prebid.floors.enabled = requestEnabled } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.enabled = accountEnabled + } accountDao.save(account) when: "PBS processes auction request" @@ -45,10 +47,15 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { then: "Bidder request bidFloor should correspond request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor - assert bidderRequest.ext?.prebid?.floors?.enabled == bidRequest.ext.prebid.floors.enabled + assert !bidderRequest.ext?.prebid?.floors?.enabled and: "PBS should not fetch rules from floors provider" assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0 + + where: + requestEnabled | accountEnabled + false | true + true | false } def "PBS should skip signalling for request without rules when ext.prebid.floors.enabled = false in request"() { @@ -85,8 +92,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorsProviderFloorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorsProviderFloorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -111,8 +118,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set invalid Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = null } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -139,8 +146,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set invalid Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = null } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -171,10 +178,10 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with skipRate" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD - data.modelGroups[0].skipRate = skipRate + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD + modelGroups[0].skipRate = skipRate } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -187,7 +194,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { then: "Bidder request bidFloor should correspond to floors provider" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - assert bidderRequest.imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + assert bidderRequest.imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + assert !bidderRequest.ext?.prebid?.floors?.skipped where: skipRate << [0, null] @@ -206,10 +214,11 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with skipRate" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD - data.modelGroups[0].skipRate = 100 + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD + modelGroups[0].skipRate = modelGroupSkipRate + skipRate = dataSkipRate } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -223,9 +232,16 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor assert bidderRequest.imp[0].bidFloorCur == bidRequest.imp[0].bidFloorCur + assert bidderRequest.ext?.prebid?.floors?.skipRate == 100 + assert bidderRequest.ext?.prebid?.floors?.skipped and: "PBS should not made signalling" assert !bidderRequest.imp[0].ext?.prebid?.floors + + where: + modelGroupSkipRate | dataSkipRate + 100 | 0 + null | 100 } def "PBS should not emit error when request has more rules than fetch.max-rules"() { @@ -326,8 +342,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorsProviderFloorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorsProviderFloorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -373,8 +389,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorsProviderFloorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorsProviderFloorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -410,8 +426,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -441,16 +457,16 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups.last().schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) - data.modelGroups.last().values = [(new Rule(siteDomain: domain).rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups.last().schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) + modelGroups.last().values = [(new Rule(siteDomain: domain).rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest) then: "Bidder request should contain 1 modelGroup" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -470,9 +486,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def bannerFloorValue = PBSUtils.randomFloorValue def videoFloorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) - data.modelGroups[0].values = [(new Rule(mediaType: BANNER).rule): bannerFloorValue, + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) + modelGroups[0].values = [(new Rule(mediaType: BANNER).rule): bannerFloorValue, (new Rule(mediaType: VIDEO).rule) : videoFloorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index b76a2b3214e..9eb4da71b24 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -32,6 +32,7 @@ import io.vertx.core.http.HttpMethod; import lombok.AllArgsConstructor; import lombok.Value; +import org.assertj.core.groups.Tuple; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -559,7 +560,7 @@ public void makeHttpRequestsShouldNotSetBidFloorCurrencyToUSDIfNull() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).doesNotContainNull() .extracting(Imp::getBidfloor, Imp::getBidfloorcur) - .containsOnly(tuple(BigDecimal.ONE, null)); + .containsOnly(tuple(BigDecimal.ONE, "USD")); } @Test @@ -2643,8 +2644,9 @@ public void makeHttpRequestsShouldReturnOnlyLineItemRequestsWithExpectedFieldsWh @Test public void makeHttpRequestsShouldFillImpExtWithFloors() { // given - final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "USD"); - + final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "JPY"); + when(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .thenReturn(BigDecimal.ONE); when(priceFloorResolver.resolve(any(), any(), any(), any(), any(), any())).thenReturn(priceFloorResult); final BidRequest bidRequest = givenBidRequest( @@ -2663,6 +2665,43 @@ public void makeHttpRequestsShouldFillImpExtWithFloors() { // then assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp).doesNotContainNull() + .extracting(Imp::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)) + .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(4001, + mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))), + RubiconImpExtRpTrack.of("", "")), null, 1, null, + RubiconImpExtPrebid.of(ExtImpPrebidFloors.of("video", BigDecimal.ONE, BigDecimal.ONE)))); + } + + @Test + public void makeHttpRequestsShouldAssumeDefaultIpfCurrencyAsUSD() { + // given + final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, null); + when(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .thenReturn(BigDecimal.ONE); + when(priceFloorResolver.resolve(any(), any(), any(), any(), any(), any())).thenReturn(priceFloorResult); + + final BidRequest bidRequest = givenBidRequest( + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .debug(1) + .floors(givenFloors(floors -> floors.data(givenFloorData( + floorData -> floorData.modelGroups(singletonList( + givenModelGroup(UnaryOperator.identity()))))))) + .build())), + builder -> builder.id("123").video(Video.builder().build()), + builder -> builder + .zoneId(4001) + .inventory(mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))))); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Ipf for imp `123` provided floor with no currency, assuming USD")); assertThat(result.getValue()).hasSize(1).doesNotContainNull() .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).doesNotContainNull() @@ -2714,9 +2753,10 @@ public void makeHttpRequestsShouldConvertBidFloor() { @Test public void makeHttpRequestsShouldFillImpExtWithFloorsWhenBothVideoAndBanner() { // given - final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "USD"); - + final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "JPY"); when(priceFloorResolver.resolve(any(), any(), any(), any(), any(), any())).thenReturn(priceFloorResult); + when(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .thenReturn(BigDecimal.ONE); final BidRequest bidRequest = givenBidRequest( builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() @@ -2752,7 +2792,7 @@ public void makeHttpRequestsShouldFillImpExtWithFloorsWhenBothVideoAndBanner() { .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(4001, mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))), RubiconImpExtRpTrack.of("", "")), null, 1, null, - RubiconImpExtPrebid.of(ExtImpPrebidFloors.of("video", BigDecimal.TEN, BigDecimal.TEN)))); + RubiconImpExtPrebid.of(ExtImpPrebidFloors.of("video", BigDecimal.ONE, BigDecimal.ONE)))); } @Test @@ -2944,7 +2984,8 @@ public void makeBidsShouldReturnBannerBidIfRequestImpHasNoVideo() throws JsonPro // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), banner, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), banner)); } @Test @@ -2963,7 +3004,8 @@ public void makeBidsShouldReturnBannerBidIfRequestImpHasBannerAndVideoButNoRequi // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), banner, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), banner)); } @Test @@ -2983,7 +3025,8 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasBannerAndVideoButAllRequi // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), video, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), video)); } @Test @@ -2999,7 +3042,8 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), video, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), video)); } @Test @@ -3116,7 +3160,8 @@ public void makeBidsShouldReturnBidWithBidIdFieldFromBidResponseIfZero() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().id("bidid1").price(ONE).build(), banner, null)); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().id("bidid1").price(ONE).build(), banner)); } @Test @@ -3136,7 +3181,8 @@ public void makeBidsShouldReturnBidWithOriginalBidIdFieldFromBidResponseIfNotZer // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().id("non-zero").price(ONE).build(), banner, null)); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().id("non-zero").price(ONE).build(), banner)); } @Test diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java index e4e6f1d4de8..e46e215b411 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java @@ -297,7 +297,7 @@ public void shouldRejectBidsHavingPriceBelowFloor() { singletonList(BidderBid.of( Bid.builder().id("bidId2").impid("impId").price(BigDecimal.TEN).build(), null, null)), singletonList(BidderError.of("Bid with id 'bidId1' was rejected by floor enforcement: " - + "price 0 is below the floor 1", BidderError.Type.generic))); + + "price 0 is below the floor 1", BidderError.Type.rejected_ipf))); } @Test diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index dac9ca871b3..fa639324124 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -6,12 +6,12 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorLocation; @@ -36,7 +36,6 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -50,8 +49,6 @@ public class BasicPriceFloorProcessorTest extends VertxTest { private PriceFloorFetcher priceFloorFetcher; @Mock private PriceFloorResolver floorResolver; - @Mock - private CurrencyConversionService conversionService; private BasicPriceFloorProcessor priceFloorProcessor; @@ -60,12 +57,11 @@ public void setUp() { priceFloorProcessor = new BasicPriceFloorProcessor( priceFloorFetcher, floorResolver, - conversionService, jacksonMapper); } @Test - public void shouldDoNothingIfPriceFloorsDisabledForAccount() { + public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForAccount() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(floorsConfig -> floorsConfig.enabled(false)), @@ -79,11 +75,13 @@ public void shouldDoNothingIfPriceFloorsDisabledForAccount() { // then verifyNoInteractions(priceFloorFetcher); - assertThat(result).isSameAs(auctionContext); + assertThat(result) + .isEqualTo(auctionContext.with(givenBidRequest( + identity(), PriceFloorRules.builder().enabled(false).build()))); } @Test - public void shouldDoNothingIfPriceFloorsDisabledForRequest() { + public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForRequest() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(identity()), @@ -97,20 +95,28 @@ public void shouldDoNothingIfPriceFloorsDisabledForRequest() { // then verifyNoInteractions(priceFloorFetcher); - assertThat(result).isSameAs(auctionContext); + assertThat(result) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getFloors) + .extracting(PriceFloorRules::getEnabled) + .isEqualTo(false); } @Test - public void shouldUseFloorsFromProviderIfPresent() { + public void shouldUseFloorsDataFromProviderIfPresent() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(identity()), givenBidRequest( identity(), - null)); + givenFloors(floors -> floors.floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ONE)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -118,13 +124,16 @@ public void shouldUseFloorsFromProviderIfPresent() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) + .floorProvider("provider.com") .floorMin(BigDecimal.ONE) + .data(providerFloorsData) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); } @Test - public void shouldNUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { + public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)), @@ -132,8 +141,10 @@ public void shouldNUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { identity(), null)); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ONE)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -141,22 +152,26 @@ public void shouldNUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors - .floorMin(BigDecimal.ONE) + .enabled(true) + .floorProvider("provider.com") + .data(providerFloorsData) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); } @Test - public void shouldNUseFloorsFromProviderIfUseDynamicDataIsTrue() { + public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(floorsConfig -> floorsConfig.useDynamicData(true)), givenBidRequest( identity(), - null)); + givenFloors(floors -> floors.floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ONE)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -164,6 +179,9 @@ public void shouldNUseFloorsFromProviderIfUseDynamicDataIsTrue() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) + .floorProvider("provider.com") + .data(providerFloorsData) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); @@ -178,17 +196,21 @@ public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() { identity(), null)); - final PriceFloorRules providerFloors = givenFloors(identity()); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); // then assertThat(extractFloors(result)) - .isEqualTo(givenFloors(floors -> floors - .fetchStatus(FetchStatus.success) - .location(PriceFloorLocation.noData))); + .extracting(PriceFloorRules::getFetchStatus) + .isEqualTo(FetchStatus.success); + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); } @Test @@ -203,8 +225,10 @@ public void shouldMergeProviderWithRequestFloors() { .enforcement(PriceFloorEnforcement.builder().enforcePbs(false).enforceRate(100).build()) .floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ZERO)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -213,12 +237,72 @@ public void shouldMergeProviderWithRequestFloors() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .enabled(true) - .enforcement(PriceFloorEnforcement.builder().enforceRate(100).build()) + .floorProvider("provider.com") + .enforcement(PriceFloorEnforcement.builder() + .enforcePbs(false) + .enforceRate(100 + ).build()) + .data(providerFloorsData) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); } + @Test + public void shouldReturnProviderFloorsWhenNotEnabledByRequestAndEnforceRateAndFloorPriceAreAbsent() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(floorsConfig -> floorsConfig.enabled(true)), + givenBidRequest( + identity(), + givenFloors(floors -> floors.data(givenFloorData(identity())).enabled(null)))); + + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + final PriceFloorRules expectedResult = + givenFloors(floors -> floors + .enabled(true) + .floorProvider("provider.com") + .data(providerFloorsData) + .fetchStatus(FetchStatus.success) + .location(PriceFloorLocation.fetch)); + + assertThat(extractFloors(result)).isEqualTo(expectedResult); + } + + @Test + public void shouldReturnFloorsWithFloorMinAndCurrencyFromRequestWhenPresent() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .enabled(true) + .floorMin(BigDecimal.ONE) + .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("USD")))))); + + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) + .containsExactly(BigDecimal.ONE, "USD"); + } + @Test public void shouldUseFloorsFromRequestIfProviderFloorsMissing() { // given @@ -236,6 +320,7 @@ public void shouldUseFloorsFromRequestIfProviderFloorsMissing() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .floorMin(BigDecimal.ONE) .location(PriceFloorLocation.request))); } @@ -256,8 +341,11 @@ public void shouldTolerateMissingRequestAndProviderFloors() { // then assertThat(extractFloors(result)) - .isEqualTo(givenFloors(floors -> floors - .location(PriceFloorLocation.noData))); + .extracting(PriceFloorRules::getEnabled) + .isEqualTo(true); + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); } @Test @@ -275,6 +363,7 @@ public void shouldNotSkipFloorsIfRootSkipRateIsOff() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .skipRate(0) .location(PriceFloorLocation.request))); } @@ -295,6 +384,7 @@ public void shouldSkipFloorsIfRootSkipRateIsOn() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .skipRate(100) + .enabled(false) .skipped(true) .location(PriceFloorLocation.request))); } @@ -316,7 +406,8 @@ public void shouldSkipFloorsIfDataSkipRateIsOn() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors - .skipRate(0) + .enabled(false) + .skipRate(100) .data(priceFloorData) .skipped(true) .location(PriceFloorLocation.request))); @@ -342,6 +433,8 @@ public void shouldSkipFloorsIfModelGroupSkipRateIsOn() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .data(priceFloorData) + .skipRate(100) + .enabled(false) .skipped(true) .location(PriceFloorLocation.request))); } @@ -385,7 +478,37 @@ public void shouldUseSelectedModelGroup() { priceFloorProcessor.enrichWithPriceFloors(auctionContext); // then - verify(floorResolver).resolve(any(), same(modelGroup), any(), any()); + final ArgumentCaptor captor = ArgumentCaptor.forClass(PriceFloorRules.class); + verify(floorResolver).resolve(any(), captor.capture(), any(), any()); + assertThat(captor.getValue()) + .extracting(PriceFloorRules::getData) + .extracting(PriceFloorData::getModelGroups) + .isEqualTo(singletonList(modelGroup)); + } + + @Test + public void shouldCopyFloorProviderValueFromDataLevel() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .floorMin(BigDecimal.ONE)))); + + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getData) + .extracting(PriceFloorData::getFloorProvider) + .isEqualTo("provider.com"); } @Test @@ -510,19 +633,30 @@ private static Imp givenImp(UnaryOperator impCustomizer) { private static PriceFloorRules givenFloors( UnaryOperator floorsCustomizer) { - return floorsCustomizer.apply(PriceFloorRules.builder()).build(); + return floorsCustomizer.apply(PriceFloorRules.builder() + .data(PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE) + .build())) + .build()) + ).build(); } private static PriceFloorData givenFloorData( UnaryOperator floorDataCustomizer) { - return floorDataCustomizer.apply(PriceFloorData.builder()).build(); + return floorDataCustomizer.apply(PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE) + .build()))).build(); } private static PriceFloorModelGroup givenModelGroup( UnaryOperator modelGroupCustomizer) { - return modelGroupCustomizer.apply(PriceFloorModelGroup.builder()).build(); + return modelGroupCustomizer.apply(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE)) + .build(); } private static PriceFloorRules extractFloors(AuctionContext auctionContext) { diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java index 80b89cf5d4f..c456dfabf51 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java @@ -21,6 +21,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorField; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; @@ -83,7 +84,7 @@ public void resolveShouldReturnNullWhenNoModelGroupSchema() { final BidRequest bidRequest = BidRequest.builder().build(); // when and then - assertThat(priceFloorResolver.resolve(bidRequest, PriceFloorModelGroup.builder().build(), + assertThat(priceFloorResolver.resolve(bidRequest, givenRules(PriceFloorModelGroup.builder().build()), Imp.builder().build(), null)).isNull(); } @@ -94,9 +95,9 @@ public void resolveShouldReturnNullWhenModelGroupSchemaFieldsIsEmpty() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", emptyList())) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -107,9 +108,9 @@ public void resolveShouldReturnNullWhenNoModelGroupSchemaFields() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", null)) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -120,10 +121,10 @@ public void resolveShouldReturnNullWhenModelGroupValuesIsEmpty() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) .values(emptyMap()) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -134,9 +135,9 @@ public void resolveShouldReturnNullWhenNoModelGroupValues() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -147,10 +148,10 @@ public void resolveShouldReturnNullWhenNoSiteDomainPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.siteDomain))) .value("siteDomain", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -163,10 +164,10 @@ public void resolveShouldReturnPriceFloorForSiteDomainPresentedBySite() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.siteDomain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -179,10 +180,10 @@ public void resolveShouldReturnPriceFloorForSiteDomainPresentedByApp() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.siteDomain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -193,10 +194,10 @@ public void resolveShouldReturnNullWhenNoPubDomainPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("pubDomain", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -211,10 +212,10 @@ public void resolveShouldReturnPriceFloorForPubDomainPresentedBySite() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -229,10 +230,10 @@ public void resolveShouldReturnPriceFloorForPubDomainPresentedByApp() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -243,10 +244,10 @@ public void resolveShouldReturnNullWhenNoDomainPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("pubDomain", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -259,10 +260,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedBySite() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -275,10 +276,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedByApp() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -293,10 +294,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedBySitePublisher() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -311,10 +312,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedByAppPublisher() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -325,10 +326,10 @@ public void resolveShouldReturnNullWhenNoBundlePresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.bundle))) .value("bundle", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -343,10 +344,10 @@ public void resolveShouldReturnPriceFloorIfBundlePresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.bundle))) .value("someBundle", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -357,10 +358,10 @@ public void resolveShouldReturnNullWhenNoChannelPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) .value("channel", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -378,10 +379,10 @@ public void resolveShouldReturnPriceFloorIfChannelPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) .value("someChannelName", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -392,10 +393,10 @@ public void resolveShouldReturnNullWhenMediaTypeDoesNotMatchRuleMediaType() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -406,12 +407,12 @@ public void resolveShouldReturnPriceFloorForCatchAllImpMediaTypeWhenImpContainsM // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("banner", BigDecimal.ONE) .value("*", BigDecimal.TEN) .value("video", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder .video(Video.builder().build())), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); @@ -424,10 +425,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsBannerAndRuleMediaTyp // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("banner", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -439,10 +440,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsVideoInStreamAndRuleM // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder().placement(1).build())), @@ -458,10 +459,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsVideoByEmptyPlacement // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder().placement(null).build())), null @@ -476,10 +477,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsVideoInStreamAndRuleM // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video-instream", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder().placement(1).build())), null).getFloorValue()) @@ -493,10 +494,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsNativeAndRuleMediaTyp // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("native", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .xNative(Native.builder().build())), null).getFloorValue()) @@ -510,10 +511,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsAudioAndRuleMediaType // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("native", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .xNative(Native.builder().build())), null).getFloorValue()) @@ -527,10 +528,10 @@ public void resolveShouldReturnNullWhenSizeDoesNotMatchRuleSize() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -541,11 +542,11 @@ public void resolveShouldReturnPriceFloorWhenMediaTypeIsBannerAndTakePriorityFor // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("100x150", BigDecimal.ONE) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(Banner.builder() .w(100) @@ -562,12 +563,12 @@ public void resolveShouldReturnPriceFloorForCatchAllWildcardWhenMultipleFormats( // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("400x500", BigDecimal.ONE) .value("*", BigDecimal.TEN) .value("250x300", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(Banner.builder() .w(100) @@ -585,10 +586,10 @@ public void resolveShouldReturnPriceFloorWhenMediaTypeIsBannerAndTakeSizesForFor // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(Banner.builder() .w(250) @@ -604,10 +605,10 @@ public void resolveShouldReturnPriceFloorWhenMediaTypeIsVideoAndTakeSizesForForm // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder() @@ -625,10 +626,10 @@ public void resolveShouldReturnNullWhenGptSlotDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("someGptSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -646,10 +647,10 @@ public void resolveShouldReturnPriceFloorIfAdserverNameIsGamAndAdSlotMatchesRule // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("someGptSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -668,10 +669,10 @@ public void resolveShouldReturnNullIfAdserverNameIsNotGamAndAdSlotMatchesRule() // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("someGptSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null)) .isNull(); } @@ -687,10 +688,10 @@ public void resolveShouldReturnPriceFloorFromPbAdSlotIfAdserverNameIsNotGamAndAd // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("somePbAdSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -702,10 +703,10 @@ public void resolveShouldReturnNullWhenPbAdSlotDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pbAdSlot))) .value("somePbAdSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -720,10 +721,10 @@ public void resolveShouldReturnPriceFloorIfPbAdSlotMatchesRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pbAdSlot))) .value("somePbAdSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -735,10 +736,10 @@ public void resolveShouldReturnNullWhenCountryDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.country))) .value("USA", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -751,10 +752,10 @@ public void resolveShouldReturnPriceFloorWhenCountryMatchesRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.country))) .value("usa", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -769,10 +770,10 @@ public void resolveShouldReturnPriceFloorWhenCountryAlpha3IsForMatchingRuleAlpha // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.country))) .value("usa", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -784,10 +785,10 @@ public void resolveShouldReturnNullWhenDeviceTypeDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("desktop", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -800,10 +801,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesPhone() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -817,10 +818,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesIPhone() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -834,10 +835,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesAndroidMobile( // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -851,10 +852,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesMobileAndroid( // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -868,10 +869,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesTablet() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -885,10 +886,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesIPad() { // when and then final BigDecimal floorValue = priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue(); assertThat(floorValue) .isEqualTo(BigDecimal.TEN); @@ -903,10 +904,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesWindowsNt() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -920,10 +921,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesAndroid() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -937,10 +938,10 @@ public void resolveShouldReturnPriceFloorForDesktopTypeWhenUaDoesNotMatchTabletO // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("desktop", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -954,14 +955,14 @@ public void resolveShouldReturnFloorWhenAllFieldsMatchExactly() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|*|300x250", BigDecimal.ONE) .value("*|banner|300x250", BigDecimal.ONE) .value("desktop|banner|300x250", BigDecimal.TEN) .value("desktop|banner|*", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -978,13 +979,13 @@ public void resolveShouldReturnFloorWhenMostAccurateWildcardMatchIsPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|*|300x250", BigDecimal.ONE) .value("desktop|banner|*", BigDecimal.TEN) .value("*|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1001,12 +1002,12 @@ public void resolveShouldReturnFloorWhenNarrowerWildcardMatchIsPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|*|*", BigDecimal.ONE) .value("*|banner|300x250", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1023,11 +1024,11 @@ public void resolveShouldReturnFloorWhenCaseIsDifferent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("deSKtop|baNNer|300X250", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1044,11 +1045,11 @@ public void resolveShouldReturnFloorWhenDelimiterIsNullAndDefaultAssumed() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of(null, List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1065,12 +1066,12 @@ public void resolveShouldReturnDefaultWhenNoMatchingRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of(null, List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("video|banner|300x250", BigDecimal.ONE) .defaultFloor(BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1089,12 +1090,12 @@ public void resolveShouldReturnFloorInRulesCurrency() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .currency("EUR") .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1110,21 +1111,21 @@ public void resolveShouldReturnFloorInRulesCurrencyIfConversionIsNotPossible() { final BidRequest bidRequest = BidRequest.builder() .device(Device.builder().ua("potential desktop type").build()) .ext(ExtRequest.of(ExtRequestPrebid.builder() - .floors(PriceFloorRules.builder() - .floorMin(BigDecimal.ONE) - .floorMinCur("UNKNOWN") - .build()) + .floors(PriceFloorRules.builder() + .floorMin(BigDecimal.ONE) + .floorMinCur("UNKNOWN") + .build()) .build())) .build(); // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .currency("EUR") .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1142,11 +1143,11 @@ public void resolveShouldReturnFloorRuleThatWasSelected() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1171,10 +1172,10 @@ public void resolveShouldReturnEffectiveFloorMinIfCurrencyIsTheSameAndAllFloorsR // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -1198,11 +1199,11 @@ public void resolveShouldReturnConvertedFloorMinInProvidedCurrencyAndFloorMinMor // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .currency("GUF") .value("appDomain", BigDecimal.valueOf(5)) - .build(), givenImp(identity()), null)) + .build()), givenImp(identity()), null)) .isEqualTo(PriceFloorResult.of("appdomain", BigDecimal.valueOf(5), BigDecimal.TEN, "GUF")); } @@ -1223,10 +1224,10 @@ public void resolveShouldReturnCorrectValueAfterRoundingUpFifthDecimalNumber() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.ZERO) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.valueOf(9.0001D)); } @@ -1247,13 +1248,22 @@ public void resolveShouldReturnCorrectValueAfterRoundingUpToWhole() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.ZERO) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } + private static PriceFloorRules givenRules(PriceFloorModelGroup modelGroup) { + + return PriceFloorRules.builder() + .data(PriceFloorData.builder() + .modelGroups(singletonList(modelGroup)) + .build()) + .build(); + } + private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id("123") diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java index d18eb51f9da..182ad127e54 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java @@ -15,11 +15,11 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.model.PriceFloorField; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.floors.model.PriceFloorSchema; -import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.metric.Metrics; @@ -90,7 +90,7 @@ public void fetchShouldReturnPriceFloorFetchedFromProviderAndCache() { final Account givenAccount = givenAccount(identity()); given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when final FetchResult fetchResult = priceFloorFetcher.fetch(givenAccount); @@ -104,7 +104,7 @@ public void fetchShouldReturnPriceFloorFetchedFromProviderAndCache() { final FetchResult priceFloorRulesCached = priceFloorFetcher.fetch(givenAccount); assertThat(priceFloorRulesCached.getFetchStatus()).isEqualTo(FetchStatus.success); - assertThat(priceFloorRulesCached.getRules()).isEqualTo(givenPriceFloorRules()); + assertThat(priceFloorRulesCached.getRulesData()).isEqualTo(givenPriceFloorData()); } @@ -118,7 +118,7 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocation( final FetchResult fetchResult = priceFloorFetcher.fetch(givenAccount(identity())); // then - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); } @@ -133,12 +133,12 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocationA final FetchResult firstInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); // then - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); } @@ -152,12 +152,12 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocationA final FetchResult firstInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); // then - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.timeout); } @@ -168,7 +168,7 @@ public void fetchShouldCacheResponseForTimeFromResponseCacheControlHeader() { .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CACHE_CONTROL, "max-age=700"), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -185,7 +185,7 @@ public void fetchShouldTakePrecedenceForTestingPropertyToCacheResponse() { .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CACHE_CONTROL, "max-age=700"), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -202,7 +202,7 @@ public void fetchShouldTakePrecedenceForTestingPropertyToCreatePeriodicTimer() { given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -219,7 +219,7 @@ public void fetchShouldTakePrecedenceForTestingPropertyToChooseRequestTimeout() given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -236,7 +236,7 @@ public void fetchShouldTakePrecedenceForMinTimeoutTestingPropertyToChooseRequest given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -251,7 +251,7 @@ public void fetchShouldSetDefaultCacheTimeWhenCacheControlHeaderCantBeParsed() { given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CACHE_CONTROL, "invalid"), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -267,7 +267,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsMalformedAndReturnErro // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoInteractions(vertx); } @@ -279,7 +279,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsBlankAndReturnErrorSta // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoInteractions(vertx); } @@ -291,7 +291,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsNotProvidedAndReturnEr // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoInteractions(vertx); } @@ -303,7 +303,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchEnabledIsFalseAndReturnNone // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.none); verifyNoInteractions(vertx); } @@ -320,12 +320,12 @@ public void fetchShouldReturnEmptyRulesAndErrorStatusForSecondCallAndCreatePerio // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -342,12 +342,12 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusAndCreatePeriodicTimerWhen // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -364,12 +364,12 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusForSecondCallAndCreatePeri // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -386,12 +386,12 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusForSecondCallAndCreatePeri // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -410,16 +410,16 @@ public void fetchShouldNotCallPriceFloorProviderWhileFetchIsAlreadyInProgress() verify(httpClient).get(anyString(), anyLong(), anyLong()); verifyNoMoreInteractions(httpClient); - assertThat(secondFetch.getRules()).isNull(); + assertThat(secondFetch.getRulesData()).isNull(); assertThat(secondFetch.getFetchStatus()).isEqualTo(FetchStatus.inprogress); fetchPromise.tryComplete( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CACHE_CONTROL, "max-age==3"), - jacksonMapper.encodeToString(givenPriceFloorRules()))); + jacksonMapper.encodeToString(givenPriceFloorData()))); - final PriceFloorRules thirdFetch = priceFloorFetcher.fetch(givenAccount(identity())).getRules(); - assertThat(thirdFetch).isEqualTo(givenPriceFloorRules()); + final PriceFloorData thirdFetch = priceFloorFetcher.fetch(givenAccount(identity())).getRulesData(); + assertThat(thirdFetch).isEqualTo(givenPriceFloorData()); } @Test @@ -428,13 +428,11 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules().toBuilder() - .data(PriceFloorData.builder() + jacksonMapper.encodeToString(PriceFloorData.builder() .modelGroups(singletonList(PriceFloorModelGroup.builder() .value("video", BigDecimal.ONE).value("banner", BigDecimal.TEN) .build())) - .build()) - .build())))); + .build())))); // when final FetchResult firstInvocationResult = @@ -442,12 +440,12 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -478,17 +476,14 @@ private static AccountPriceFloorsFetchConfig givenFetchConfig( .build(); } - private PriceFloorRules givenPriceFloorRules() { - return PriceFloorRules.builder() - .data(PriceFloorData.builder() - .currency("USD") - .modelGroups(singletonList(PriceFloorModelGroup.builder() - .modelVersion("model version 1.0") - .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) - .value("banner", BigDecimal.TEN) - .currency("EUR").build())) - .build()) - .skipRate(60) + private PriceFloorData givenPriceFloorData() { + return PriceFloorData.builder() + .currency("USD") + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .modelVersion("model version 1.0") + .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) + .value("banner", BigDecimal.TEN) + .currency("EUR").build())) .build(); } } diff --git a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java new file mode 100644 index 00000000000..90689b73ce1 --- /dev/null +++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java @@ -0,0 +1,200 @@ +package org.prebid.server.floors; + +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorModelGroup; +import org.prebid.server.floors.model.PriceFloorRules; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class PriceFloorRulesValidatorTest extends VertxTest { + + @Test + public void validateShouldThrowExceptionOnInvalidRootSkipRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.skipRate(-1)); + + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor root skipRate must be in range(0-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionWhenFloorMinPresentAndLessThanZero() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRules( + rulesBuilder -> rulesBuilder.floorMin(BigDecimal.valueOf(-1))); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor floorMin must be positive float, but was -1"); + } + + @Test + public void validateShouldThrowExceptionWhenDataIsAbsent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.data(null)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor rules data must be present"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData(dataBuilder -> dataBuilder.skipRate(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor data skipRate must be in range(0-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnAbsentDataModelGroups() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData( + dataBuilder -> dataBuilder.modelGroups(null)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor rules should contain at least one model group"); + } + + @Test + public void validateShouldThrowExceptionOnEmptyDataModelGroups() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData( + dataBuilder -> dataBuilder.modelGroups(Collections.emptyList())); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor rules should contain at least one model group"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataModelGroupModelWeightWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.modelWeight(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor modelGroup modelWeight must be in range(1-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataModelGroupSkipRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.skipRate(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor modelGroup skipRate must be in range(0-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataModelGroupDefaultFloorWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.defaultFloor(BigDecimal.valueOf(-1))); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor modelGroup default must be positive float, but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnEmptyModelGroupValues() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + PriceFloorModelGroup.PriceFloorModelGroupBuilder::clearValues); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor rules values can't be null or empty, but were {}"); + } + + @Test + public void validateShouldThrowExceptionWhenModelGroupValuesSizeGreaterThanMaxRules() { + // given + final Map modelGroupValues = Map.of( + "v1", BigDecimal.TEN, + "v2", BigDecimal.TEN); + + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.clearValues().values(modelGroupValues)); + + final int maxRules = modelGroupValues.size() - 1; + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, maxRules)) + .withMessage( + "Price floor rules number %s exceeded its maximum number %s", + modelGroupValues.size(), + maxRules); + } + + private static PriceFloorRules givenPriceFloorRulesWithDataModelGroups( + UnaryOperator... modelGroupBuilders) { + + final PriceFloorModelGroup.PriceFloorModelGroupBuilder validModelGroupBuilder = + PriceFloorModelGroup.builder() + .modelWeight(10) + .skipRate(10) + .defaultFloor(BigDecimal.TEN) + .values(Map.of("value", BigDecimal.TEN)); + + final List modelGroups = Arrays.stream(modelGroupBuilders) + .map(modelGroupBuilder -> modelGroupBuilder.apply(validModelGroupBuilder).build()) + .collect(Collectors.toList()); + + return givenPriceFloorRulesWithData(dataBuilder -> dataBuilder.modelGroups(modelGroups)); + } + + private static PriceFloorRules givenPriceFloorRulesWithData( + UnaryOperator dataBuilder) { + + return givenPriceFloorRules(UnaryOperator.identity(), dataBuilder); + } + + private static PriceFloorRules givenPriceFloorRules( + UnaryOperator rulesBuilder) { + + return givenPriceFloorRules(rulesBuilder, UnaryOperator.identity()); + } + + private static PriceFloorRules givenPriceFloorRules( + UnaryOperator rulesBuilder, + UnaryOperator dataBuilder) { + + final PriceFloorRules priceFloorRules = PriceFloorRules.builder() + .skipRate(10) + .floorMin(BigDecimal.TEN) + .data(dataBuilder.apply(PriceFloorData.builder()).build()) + .build(); + + return rulesBuilder.apply(priceFloorRules.toBuilder()).build(); + } +} diff --git a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java index 3490e51d879..450578faf55 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java @@ -19,6 +19,7 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -39,6 +40,13 @@ public void setUp() { metrics); } + @Test + public void priceFloorsConfigResolverShouldNotCreateInstanceIfDefaultAccountIsInvalid() { + assertThatIllegalArgumentException().isThrownBy(() -> new PriceFloorsConfigResolver( + "{", + metrics)); + } + @Test public void updateFloorsConfigShouldNotChangeAccountIfConfigIsValid() { // when @@ -210,6 +218,86 @@ public void updateFloorsConfigShouldReturnDefaultConfigIfMaxFileSizeMoreThanMaxi verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); } + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountEnforceFloorRateIsNotPresent() { + // given + final Account givenAccount = Account.builder() + .id("some-id") + .auction(AccountAuctionConfig.builder() + .priceFloors(AccountPriceFloorsConfig.builder() + .enforceFloorsRate(null).build()) + .build()) + .build(); + + // when + final Future future = testingInstance.updateFloorsConfig(givenAccount); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountMaxFileSizeIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.maxFileSize(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountPeriodicSecIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.periodSec(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountFetchTimeoutIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.timeout(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountMaxRulesIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.maxRules(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountMaxAgeSecIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.maxAgeSec(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + private static Account accountWithFloorsFetchConfig( UnaryOperator configCustomizer) { return Account.builder() @@ -235,7 +323,14 @@ private static Account withDefaultFloorsConfig(UnaryOperator bidders, Customization... customizations) throws IOException, JSONException { - final List fullCustomizations = new ArrayList<>(Arrays.asList(customizations)); - fullCustomizations.add(new Customization("ext.prebid.auctiontimestamp", (o1, o2) -> true)); - fullCustomizations.add(new Customization("ext.responsetimemillis.cache", (o1, o2) -> true)); - String expectedRequest = replaceStaticInfo(jsonFrom(file)); - for (String bidder : bidders) { - expectedRequest = replaceBidderRelatedStaticInfo(expectedRequest, bidder); - fullCustomizations.add(new Customization( - String.format("ext.responsetimemillis.%s", bidder), (o1, o2) -> true)); - } - JSONAssert.assertEquals(expectedRequest, response.asString(), - new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, - fullCustomizations.toArray(new Customization[0]))); + IntegrationTestsUtil.assertJsonEquals( + file, + response, + bidders, + (json, bidder) -> replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT), + IntegrationTest::replaceStaticInfo, + customizations); } private static String replaceStaticInfo(String json) { - return json.replaceAll("\\{\\{ cache.endpoint }}", CACHE_ENDPOINT) .replaceAll("\\{\\{ cache.resource_url }}", CACHE_ENDPOINT + "?uuid=") .replaceAll("\\{\\{ cache.host }}", HOST_AND_PORT) @@ -271,12 +258,6 @@ private static String replaceStaticInfo(String json) { .replaceAll("\\{\\{ event.url }}", "http://localhost:8080/event?"); } - private static String replaceBidderRelatedStaticInfo(String json, String bidder) { - - return json.replaceAll("\\{\\{ " + bidder + "\\.exchange_uri }}", - "http://" + HOST_AND_PORT + "/" + bidder + "-exchange"); - } - static BidCacheRequestPattern equalToBidCacheRequest(String json) { return new BidCacheRequestPattern(json); } diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java new file mode 100644 index 00000000000..8d6620d0869 --- /dev/null +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -0,0 +1,113 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.json.JSONException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.VertxTest; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.IntegrationTestsUtil; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.util.IntegrationTestsUtil.assertJsonEquals; +import static org.prebid.server.util.IntegrationTestsUtil.jsonFrom; +import static org.prebid.server.util.IntegrationTestsUtil.responseFor; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RunWith(SpringRunner.class) +@TestPropertySource( + value = {"test-application.properties"}, + properties = {"price-floors.enabled=true", "http.port=8050", "admin.port=0"} +) +public class PriceFloorsTest extends VertxTest { + + private static final int APP_PORT = 8050; + private static final int WIREMOCK_PORT = 8090; + + private static final String PRICE_FLOORS = "Price Floors Test"; + private static final String FLOORS_FROM_REQUEST = "Floors from request"; + private static final String FLOORS_FROM_PROVIDER = "Floors from provider"; + + private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + + @ClassRule + public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule(options().port(WIREMOCK_PORT)); + + @BeforeClass + public static void setUp() throws IOException { + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/periodic-update")) + .willReturn(aResponse().withBody(jsonFrom("storedrequests/test-periodic-refresh.json")))); + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/currency-rates")) + .willReturn(aResponse().withBody(jsonFrom("currency/latest.json")))); + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/floors-provider")) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/provided-floors.json")))); + } + + @Test + public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) + .inScenario(PRICE_FLOORS) + .whenScenarioStateIs(STARTED) + .withRequestBody(equalToJson(jsonFrom("openrtb2/floors/floors-test-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/floors-test-bid-response.json"))) + .willSetStateTo(FLOORS_FROM_REQUEST)); + + // when + final Response firstResponse = responseFor( + "openrtb2/floors/floors-test-auction-request-1.json", + Endpoint.openrtb2_auction, + SPEC); + + // then + assertJsonEquals( + "openrtb2/floors/floors-test-auction-response.json", + firstResponse, + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); + + // given + final StubMapping stubMapping = WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) + .inScenario(PRICE_FLOORS) + .whenScenarioStateIs(FLOORS_FROM_REQUEST) + .withRequestBody(equalToJson(jsonFrom("openrtb2/floors/floors-test-bid-request-2.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/floors-test-bid-response.json"))) + .willSetStateTo(FLOORS_FROM_PROVIDER)); + + // when + final Response secondResponse = responseFor( + "openrtb2/floors/floors-test-auction-request-2.json", + Endpoint.openrtb2_auction, + SPEC); + + // then + assertThat(stubMapping.getNewScenarioState()).isEqualTo(FLOORS_FROM_PROVIDER); + assertJsonEquals( + "openrtb2/floors/floors-test-auction-response.json", + secondResponse, + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); + } + + private static String replaceBidderRelatedStaticInfo(String json, String bidder) { + return IntegrationTestsUtil.replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT); + } +} diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java index 34aaced63e7..8fe44bad80d 100644 --- a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -51,7 +51,7 @@ public void setUp() { public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsNull() { // given enrichingApplicationSettings = - new EnrichingApplicationSettings(null, delegate, priceFloorsConfigResolver, jsonMerger); + new EnrichingApplicationSettings(true, null, delegate, priceFloorsConfigResolver, jsonMerger); final Account returnedAccount = Account.builder().build(); given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(returnedAccount)); @@ -70,6 +70,7 @@ public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsNull() { public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsEmpty() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + true, "{}", delegate, priceFloorsConfigResolver, @@ -92,6 +93,7 @@ public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsEmpty() { public void getAccountByIdShouldMergeAccountWithDefaultAccount() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + true, "{\"auction\": {\"banner-cache-ttl\": 100}," + "\"privacy\": {\"gdpr\": {\"enabled\": true, \"channel-enabled\": {\"web\": false}}}}", delegate, @@ -133,6 +135,7 @@ public void getAccountByIdShouldMergeAccountWithDefaultAccount() { public void getAccountByIdShouldReturnDefaultAccountWhenDelegateFailed() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + false, "{\"auction\": {\"banner-cache-ttl\": 100}}", delegate, priceFloorsConfigResolver, @@ -152,10 +155,30 @@ public void getAccountByIdShouldReturnDefaultAccountWhenDelegateFailed() { .build()); } + @Test + public void getAccountByIdShouldReturnFailedFutureWhenDelegateFailedAndEnforceValidAccountIsTrue() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + true, + "{\"auction\": {\"banner-cache-ttl\": 100}}", + delegate, + priceFloorsConfigResolver, + jsonMerger); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.failedFuture("Exception")); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isFailed(); + } + @Test public void getAccountByIdShouldPassOnFailureWhenDefaultAccountIsEmpty() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + true, "{}", delegate, priceFloorsConfigResolver, diff --git a/src/test/java/org/prebid/server/util/IntegrationTestsUtil.java b/src/test/java/org/prebid/server/util/IntegrationTestsUtil.java new file mode 100644 index 00000000000..5c20b3cb6d1 --- /dev/null +++ b/src/test/java/org/prebid/server/util/IntegrationTestsUtil.java @@ -0,0 +1,84 @@ +package org.prebid.server.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.json.JSONException; +import org.prebid.server.it.IntegrationTest; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.model.Endpoint; +import org.skyscreamer.jsonassert.Customization; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.comparator.CustomComparator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class IntegrationTestsUtil { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + private IntegrationTestsUtil() { + } + + public static void assertJsonEquals(String file, + Response response, + List bidders, + BiFunction bidderStaticInfoUpdater, + Function staticInfoUpdater, + Customization... customizations) throws IOException, JSONException { + + final List fullCustomizations = new ArrayList<>(Arrays.asList(customizations)); + fullCustomizations.add(new Customization("ext.prebid.auctiontimestamp", (o1, o2) -> true)); + fullCustomizations.add(new Customization("ext.responsetimemillis.cache", (o1, o2) -> true)); + + String expectedRequest = staticInfoUpdater.apply(jsonFrom(file)); + for (String bidder : bidders) { + expectedRequest = bidderStaticInfoUpdater.apply(expectedRequest, bidder); + fullCustomizations.add(new Customization( + String.format("ext.responsetimemillis.%s", bidder), (o1, o2) -> true)); + } + + JSONAssert.assertEquals( + expectedRequest, + response.asString(), + new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, fullCustomizations.toArray(new Customization[0]))); + } + + public static void assertJsonEquals(String file, + Response response, + List bidders, + BiFunction bidderStaticInfoUpdater, + Customization... customizations) throws IOException, JSONException { + + assertJsonEquals(file, response, bidders, bidderStaticInfoUpdater, Function.identity(), customizations); + } + + public static String replaceBidderRelatedStaticInfo(String json, String bidder, int wiremockPort) { + return json.replaceAll("\\{\\{ " + bidder + "\\.exchange_uri }}", + "http://localhost:" + wiremockPort + "/" + bidder + "-exchange"); + } + + public static Response responseFor(String file, Endpoint endpoint, RequestSpecification requestSpecification) + throws IOException { + + return RestAssured.given(requestSpecification) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "193.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + .body(jsonFrom(file)) + .post(endpoint.value()); + } + + public static String jsonFrom(String file) throws IOException { + // workaround to clear formatting + return MAPPER.writeValueAsString(MAPPER.readTree(IntegrationTest.class.getResourceAsStream(file))); + } +} diff --git a/src/test/resources/org/prebid/server/functional/floor-rules.json b/src/test/resources/org/prebid/server/functional/floor-rules.json index 8bd346f25bf..194c83ebc91 100644 --- a/src/test/resources/org/prebid/server/functional/floor-rules.json +++ b/src/test/resources/org/prebid/server/functional/floor-rules.json @@ -1,157 +1,155 @@ { - "data": { - "floorProvider": "rubicon", - "modelGroups": [ - { - "modelWeight": 10, - "modelVersion": "mlcp-v1@2022-03-02-21", - "schema": { - "fields": [ - "domain", - "mediaType", - "gptSlot" - ], - "delimiter": "|" - }, - "values": { - "example.com|banner|/111/k/categorytop/footer_left/300x250": 0.07, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_15": 0.02, - "s.example.com|banner|/111/ks/categorytop/300x250": 0.02, - "bbs.example.com|banner|/111/k/itemview/bbs/footer_left/300x250": 0.04, - "example5.com|banner|/111/t/shop/300x600": 0.02, - "example5.com|banner|/111/t/list/search_footer_left_300x250": 0.03, - "example2.com|banner|/111/kinarinopc/article/300x250": 0.18, - "s.example.com|banner|/111/ks/itemview/h/footer/300x250": 0.03, - "example6.com|banner|/111/cg/top_3rd_300x250": 0.06, - "example5.com|banner|/111/t/shop/1st300x250": 0.02, - "example.com|banner|/111/k/ranking/footer_right/300x250": 0.06, - "review.example.com|banner|/111/k/itemview/review/footer_left/300x250": 0.05, - "bbs.example.com|banner|/111/k/itemview/footer_right/300x250": 0.04, - "example7.com|banner|/111/e/contents/footer_left_300x250": 0.02, - "s.example.com|banner|/111/ks/news/300x250": 0.03, - "s.example.com|banner|/111/ks/categorytop/footer/300x250": 0.02, - "example6.com|banner|/111/cgs/ros/300x250": 0.01, - "example7.com|banner|/111/e/contents/footer_728x90": 0.06, - "example.com|banner|/111/k/categorytop/footer_right/300x250": 0.06, - "s.example.com|banner|/111/ks/top_320x50": 0.02, - "example5.com|banner|/111/t/map/middle_468x60": 0.04, - "example4.com|*|*": 0.07, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_6": 0.06, - "news.example.com|banner|/111/k/news/footer_right/300x250": 0.08, - "example4.com|banner|/111/kmag/1st_300x250": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_4": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_1": 0.07, - "example.com|banner|/111/k/btf/tv/footer_right_300x250": 0.01, - "s.example5.com|banner|/111/ts/list/300x250": 0.02, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_5": 0.06, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_10": 0.03, - "example.com|banner|/111/k/global_search/footer_right/300x250": 0.06, - "s.akiba-souken.com|banner|/111/as/1st_300x250": 0.03, - "bbs.example.com|banner|/111/k/itemview/bbs/160x600": 0.06, - "example.com|banner|/111/k/tv_728x90": 0.06, - "example3.com|*|*": 0.02, - "s.example.com|banner|/111/ks/ranking/middle_20/300x250": 0.05, - "s.example.com|banner|/111/ks/itemview/review/300x250_9": 0.03, - "example5.com|banner|/111/t/map/middle_left_300x250": 0.04, - "s.example.com|banner|/111/ks/itemview/review/300x250_12": 0.04, - "example.com|banner|/111/k/ranking/728x90": 0.06, - "example6.com|banner|/111/cg/ros/footer_right_300x250": 0.07, - "example6.com|banner|/111/cg/top_300x250": 0.09, - "example7.com|banner|/111/es/overlay/320x50": 0.07, - "s.example.com|banner|/111/ks/itemview/bbs/300x250": 0.08, - "example5.com|banner|/111/t/list/search_footer_right_300x250": 0.04, - "example.com|banner|/111/k/pricemenu/728x90": 0.06, - "s.example.com|banner|/111/ks/itemview/320x50_lazytest": 0.03, - "search.example.com|banner|/111/ks/itemlist/320x50": 0.03, - "s.example.com|banner|/111/ks/categorytop/middle/320x50": 0.06, - "example.com|*|*": 0.04, - "example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/footer_right/300x250": 0.04, - "s.example.com|banner|/111/ks/itemview/review/300x250_3": 0.05, - "example4.com|banner|/111/kmag/footer_left_300x250": 0.08, - "s.example.com|banner|/111/ks/itemview/bbs/300x250_2": 0.08, - "example.com|banner|/111/k/itemlist/728x90": 0.07, - "example.com|banner|/111/k/ranking/middle/left/300x250": 0.09, - "search.example.com|banner|/111/k/itemlist/160x600": 0.12, - "example2.com|banner|/111/kinarinopc/top_300x250": 0.01, - "s.example.com|banner|/111/ks/itemlist/footer/300x250": 0.02, - "example2.com|banner|/111/kinarino/login": 0.09, - "example5.com|banner|/111/t/special/4th_300x250": 0.02, - "s.example.com|banner|/111/ks/news/320x50": 0.05, - "s.example.com|banner|/111/ks/itemview/review/300x250_6": 0.03, - "example.com|banner|/111/k/ranking/footer_left/300x250": 0.03, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_2": 0.06, - "example4.com|banner|/111/kmag/3rd_300x250": 0.08, - "s.example.com|banner|/111/ks/itemview/300x250": 0.04, - "example.com|banner|/111/k/itemview/footer_left/300x250": 0.05, - "review.example.com|banner|/111/k/itemview/review/160x600": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_5": 0.06, - "example5.com|banner|/111/t/shop/shop_footer_left_300x250": 0.02, - "s.example5.com|banner|/111/ts/shop/middle/300x250": 0.04, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_6": 0.01, - "s.example.com|banner|/111/ks/ranking/middle_30/300x250": 0.03, - "example.com|banner|/111/k/top_2nd_300x250": 0.01, - "search.example.com|banner|/111/ks/itemlist/middle/320x50": 0.07, - "search.example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, - "s.example.com|banner|/111/ks/categorytop/320x50": 0.02, - "s.example.com|banner|/111/ks/pricemenu/320x50": 0.02, - "example.com|banner|/111/k/specsearch/footer/728x90": 0.01, - "example4.com|banner|/111/4ts/ros/video_320x180": 0.03, - "example5.com|banner|/111/t/matome/article/300x250": 0.04, - "s.example.com|banner|/111/ks/ranking/middle_10/300x250": 0.07, - "s.example.com|banner|/111/ks/ranking/320x50": 0.03, - "example.com|banner|/111/k/categorytop/300x250": 0.08, - "s.example.com|banner|/111/ks/itemview/h/320x50": 0.03, - "example2.com|banner|/111/kinarino/article": 0.13, - "anime.example7.com|banner|/111/ahs/overlay/320x50": 0.09, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_3": 0.06, - "example.com|banner|/111/k/pricemenu/footer_right/300x250": 0.06, - "s.example.com|banner|/111/ks/itemview/bbs/footer/300x250": 0.03, - "s.example.com|banner|/111/ks/global_search/300x250": 0.06, - "example2.com|*|*": 0.03, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_5": 0.08, - "review.example.com|*|*": 0.04, - "s.example.com|banner|/111/ks/itemview/review/footer/300x250": 0.03, - "example6.com|banner|/111/cg/ros/footer_left_300x250": 0.04, - "search.example.com|banner|/111/k/itemlist/footer_left/300x250": 0.04, - "example.com|banner|/111/k/itemview/h/160x600": 0.06, - "review.example.com|banner|/111/k/itemview/review/728x90": 0.05, - "s.example.com|banner|/111/ks/tv/overlay_320x50": 0.04, - "news.example.com|*|*": 0.07, - "example.com|banner|/111/k/ranking/middle/right/300x250": 0.03, - "s.example.com|banner|/111/ks/itemlist/300x250": 0.03, - "example6.com|*|*": 0.02, - "example4.com|banner|/111/ksmag/footer_300x250": 0.06, - "example7.com|banner|/111/e/overlay/728x90": 0.03, - "example4.com|banner|/111/kmag/2nd_300x250": 0.09, - "s.example.com|banner|/111/ks/itemview/review/300x250": 0.04, - "example.com|banner|/111/k/categorytop/728x90": 0.06, - "example5.com|banner|/111/t/shop/shop_footer_right_300x250": 0.02, - "bbs.example.com|banner|/111/k/itemview/footer_left/300x250": 0.04, - "*|*|*": 0.01, - "bbs.example.com|*|*": 0.02, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_20": 0.06, - "example3.com|banner|/111/icotto_sp/article/footer_1st300x250": 0.04, - "s.example.com|banner|/111/ks/itemlist/320x50": 0.04, - "s.example.com|banner|/111/ks/itemview/footer/300x250": 0.03, - "s.example.com|banner|/111/ks/itemview/h/300x250": 0.03, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_3": 0.06, - "example3.com|banner|/111/icotto_pc/2nd_300x250": 0.08, - "example.com|banner|/111/k/pricemenu/300x250": 0.06, - "s.example.com|banner|/111/ks/tv/middle_300x250": 0.03, - "example7.com|banner|/111/es/contents/footer_buzz_300x250": 0.02, - "example.com|banner|/111/k/itemview/spec/160x600": 0.07, - "example5.com|banner|/111/t/map/middle_right_300x250": 0.04, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_2": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_4": 0.06, - "search.example.com|*|*": 0.02 - }, - "default": 0.01 - } - ], - "modelTimestamp": 1646254800, - "currency": "USD", - "skipRate": 0, - "floorsSchemaVersion": 2 - } + "floorProvider": "rubicon", + "modelGroups": [ + { + "modelWeight": 10, + "modelVersion": "mlcp-v1@2022-03-02-21", + "schema": { + "fields": [ + "domain", + "mediaType", + "gptSlot" + ], + "delimiter": "|" + }, + "values": { + "example.com|banner|/111/k/categorytop/footer_left/300x250": 0.07, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_15": 0.02, + "s.example.com|banner|/111/ks/categorytop/300x250": 0.02, + "bbs.example.com|banner|/111/k/itemview/bbs/footer_left/300x250": 0.04, + "example5.com|banner|/111/t/shop/300x600": 0.02, + "example5.com|banner|/111/t/list/search_footer_left_300x250": 0.03, + "example2.com|banner|/111/kinarinopc/article/300x250": 0.18, + "s.example.com|banner|/111/ks/itemview/h/footer/300x250": 0.03, + "example6.com|banner|/111/cg/top_3rd_300x250": 0.06, + "example5.com|banner|/111/t/shop/1st300x250": 0.02, + "example.com|banner|/111/k/ranking/footer_right/300x250": 0.06, + "review.example.com|banner|/111/k/itemview/review/footer_left/300x250": 0.05, + "bbs.example.com|banner|/111/k/itemview/footer_right/300x250": 0.04, + "example7.com|banner|/111/e/contents/footer_left_300x250": 0.02, + "s.example.com|banner|/111/ks/news/300x250": 0.03, + "s.example.com|banner|/111/ks/categorytop/footer/300x250": 0.02, + "example6.com|banner|/111/cgs/ros/300x250": 0.01, + "example7.com|banner|/111/e/contents/footer_728x90": 0.06, + "example.com|banner|/111/k/categorytop/footer_right/300x250": 0.06, + "s.example.com|banner|/111/ks/top_320x50": 0.02, + "example5.com|banner|/111/t/map/middle_468x60": 0.04, + "example4.com|*|*": 0.07, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_6": 0.06, + "news.example.com|banner|/111/k/news/footer_right/300x250": 0.08, + "example4.com|banner|/111/kmag/1st_300x250": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_4": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_1": 0.07, + "example.com|banner|/111/k/btf/tv/footer_right_300x250": 0.01, + "s.example5.com|banner|/111/ts/list/300x250": 0.02, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_5": 0.06, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_10": 0.03, + "example.com|banner|/111/k/global_search/footer_right/300x250": 0.06, + "s.akiba-souken.com|banner|/111/as/1st_300x250": 0.03, + "bbs.example.com|banner|/111/k/itemview/bbs/160x600": 0.06, + "example.com|banner|/111/k/tv_728x90": 0.06, + "example3.com|*|*": 0.02, + "s.example.com|banner|/111/ks/ranking/middle_20/300x250": 0.05, + "s.example.com|banner|/111/ks/itemview/review/300x250_9": 0.03, + "example5.com|banner|/111/t/map/middle_left_300x250": 0.04, + "s.example.com|banner|/111/ks/itemview/review/300x250_12": 0.04, + "example.com|banner|/111/k/ranking/728x90": 0.06, + "example6.com|banner|/111/cg/ros/footer_right_300x250": 0.07, + "example6.com|banner|/111/cg/top_300x250": 0.09, + "example7.com|banner|/111/es/overlay/320x50": 0.07, + "s.example.com|banner|/111/ks/itemview/bbs/300x250": 0.08, + "example5.com|banner|/111/t/list/search_footer_right_300x250": 0.04, + "example.com|banner|/111/k/pricemenu/728x90": 0.06, + "s.example.com|banner|/111/ks/itemview/320x50_lazytest": 0.03, + "search.example.com|banner|/111/ks/itemlist/320x50": 0.03, + "s.example.com|banner|/111/ks/categorytop/middle/320x50": 0.06, + "example.com|*|*": 0.04, + "example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/footer_right/300x250": 0.04, + "s.example.com|banner|/111/ks/itemview/review/300x250_3": 0.05, + "example4.com|banner|/111/kmag/footer_left_300x250": 0.08, + "s.example.com|banner|/111/ks/itemview/bbs/300x250_2": 0.08, + "example.com|banner|/111/k/itemlist/728x90": 0.07, + "example.com|banner|/111/k/ranking/middle/left/300x250": 0.09, + "search.example.com|banner|/111/k/itemlist/160x600": 0.12, + "example2.com|banner|/111/kinarinopc/top_300x250": 0.01, + "s.example.com|banner|/111/ks/itemlist/footer/300x250": 0.02, + "example2.com|banner|/111/kinarino/login": 0.09, + "example5.com|banner|/111/t/special/4th_300x250": 0.02, + "s.example.com|banner|/111/ks/news/320x50": 0.05, + "s.example.com|banner|/111/ks/itemview/review/300x250_6": 0.03, + "example.com|banner|/111/k/ranking/footer_left/300x250": 0.03, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_2": 0.06, + "example4.com|banner|/111/kmag/3rd_300x250": 0.08, + "s.example.com|banner|/111/ks/itemview/300x250": 0.04, + "example.com|banner|/111/k/itemview/footer_left/300x250": 0.05, + "review.example.com|banner|/111/k/itemview/review/160x600": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_5": 0.06, + "example5.com|banner|/111/t/shop/shop_footer_left_300x250": 0.02, + "s.example5.com|banner|/111/ts/shop/middle/300x250": 0.04, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_6": 0.01, + "s.example.com|banner|/111/ks/ranking/middle_30/300x250": 0.03, + "example.com|banner|/111/k/top_2nd_300x250": 0.01, + "search.example.com|banner|/111/ks/itemlist/middle/320x50": 0.07, + "search.example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, + "s.example.com|banner|/111/ks/categorytop/320x50": 0.02, + "s.example.com|banner|/111/ks/pricemenu/320x50": 0.02, + "example.com|banner|/111/k/specsearch/footer/728x90": 0.01, + "example4.com|banner|/111/4ts/ros/video_320x180": 0.03, + "example5.com|banner|/111/t/matome/article/300x250": 0.04, + "s.example.com|banner|/111/ks/ranking/middle_10/300x250": 0.07, + "s.example.com|banner|/111/ks/ranking/320x50": 0.03, + "example.com|banner|/111/k/categorytop/300x250": 0.08, + "s.example.com|banner|/111/ks/itemview/h/320x50": 0.03, + "example2.com|banner|/111/kinarino/article": 0.13, + "anime.example7.com|banner|/111/ahs/overlay/320x50": 0.09, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_3": 0.06, + "example.com|banner|/111/k/pricemenu/footer_right/300x250": 0.06, + "s.example.com|banner|/111/ks/itemview/bbs/footer/300x250": 0.03, + "s.example.com|banner|/111/ks/global_search/300x250": 0.06, + "example2.com|*|*": 0.03, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_5": 0.08, + "review.example.com|*|*": 0.04, + "s.example.com|banner|/111/ks/itemview/review/footer/300x250": 0.03, + "example6.com|banner|/111/cg/ros/footer_left_300x250": 0.04, + "search.example.com|banner|/111/k/itemlist/footer_left/300x250": 0.04, + "example.com|banner|/111/k/itemview/h/160x600": 0.06, + "review.example.com|banner|/111/k/itemview/review/728x90": 0.05, + "s.example.com|banner|/111/ks/tv/overlay_320x50": 0.04, + "news.example.com|*|*": 0.07, + "example.com|banner|/111/k/ranking/middle/right/300x250": 0.03, + "s.example.com|banner|/111/ks/itemlist/300x250": 0.03, + "example6.com|*|*": 0.02, + "example4.com|banner|/111/ksmag/footer_300x250": 0.06, + "example7.com|banner|/111/e/overlay/728x90": 0.03, + "example4.com|banner|/111/kmag/2nd_300x250": 0.09, + "s.example.com|banner|/111/ks/itemview/review/300x250": 0.04, + "example.com|banner|/111/k/categorytop/728x90": 0.06, + "example5.com|banner|/111/t/shop/shop_footer_right_300x250": 0.02, + "bbs.example.com|banner|/111/k/itemview/footer_left/300x250": 0.04, + "*|*|*": 0.01, + "bbs.example.com|*|*": 0.02, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_20": 0.06, + "example3.com|banner|/111/icotto_sp/article/footer_1st300x250": 0.04, + "s.example.com|banner|/111/ks/itemlist/320x50": 0.04, + "s.example.com|banner|/111/ks/itemview/footer/300x250": 0.03, + "s.example.com|banner|/111/ks/itemview/h/300x250": 0.03, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_3": 0.06, + "example3.com|banner|/111/icotto_pc/2nd_300x250": 0.08, + "example.com|banner|/111/k/pricemenu/300x250": 0.06, + "s.example.com|banner|/111/ks/tv/middle_300x250": 0.03, + "example7.com|banner|/111/es/contents/footer_buzz_300x250": 0.02, + "example.com|banner|/111/k/itemview/spec/160x600": 0.07, + "example5.com|banner|/111/t/map/middle_right_300x250": 0.04, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_2": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_4": 0.06, + "search.example.com|*|*": 0.02 + }, + "default": 0.01 + } + ], + "modelTimestamp": 1646254800, + "currency": "USD", + "skipRate": 0, + "floorsSchemaVersion": 2 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json new file mode 100644 index 00000000000..ba09e5f9846 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "cid": "AAXCID", + "crid": "12345678" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json new file mode 100644 index 00000000000..4b1ecda6ece --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json @@ -0,0 +1,22 @@ + { + "id": "tid", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid01" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json new file mode 100644 index 00000000000..7e76123f01b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json @@ -0,0 +1,24 @@ +{ + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "aax": { + "cid": "AAXCID", + "crid": "12345678" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json new file mode 100644 index 00000000000..510834b5e2e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json @@ -0,0 +1,38 @@ +{ + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.5 + } + } + ], + "seat": "aax", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "aax": "{{ aax.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json new file mode 100644 index 00000000000..a19b6560a42 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json @@ -0,0 +1,67 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "USD", + "ext": { + "generic": { + } + } + } + ], + "site": { + "publisher": { + "id": "12001" + } + }, + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "floors": { + "data": { + "modelGroups": [ + { + "currency": "NZD", + "modelWeight": 100, + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + } + }, + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json new file mode 100644 index 00000000000..3c47ca50e00 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json @@ -0,0 +1,43 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "USD", + "ext": { + "generic": { + } + } + } + ], + "site": { + "publisher": { + "id": "12001" + } + }, + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "floors": { + "floorMinCur": "NZD" + }, + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json new file mode 100644 index 00000000000..179a08e5cb1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 8.399, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 13, + "origbidcur":"GBP" + } + } + ], + "seat": "generic", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "generic": "{{ generic.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json new file mode 100644 index 00000000000..1cebf4f1ffd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json @@ -0,0 +1,96 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "NZD", + "ext": { + "prebid": { + "floors": { + "floorRule": "www.example.com|banner|320x250", + "floorRuleValue": 2, + "floorValue": 4 + } + }, + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "12001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + }, + "floors": { + "data": { + "modelGroups": [ + { + "currency": "NZD", + "modelWeight": 100, + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + }, + "enabled": true, + "fetchStatus": "inprogress", + "location": "request" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json new file mode 100644 index 00000000000..b8f0bf034a4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json @@ -0,0 +1,96 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "NZD", + "ext": { + "prebid": { + "floors": { + "floorRule": "www.example.com|banner|320x250", + "floorRuleValue": 2, + "floorValue": 4 + } + }, + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "12001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + }, + "floors": { + "data": { + "modelGroups": [ + { + "currency": "NZD", + "modelWeight": 100, + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + }, + "enabled": true, + "fetchStatus": "success", + "location": "fetch" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json new file mode 100644 index 00000000000..f6559262b7d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "cur": "GBP", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 13, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json new file mode 100644 index 00000000000..3c4dc6adb9a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json @@ -0,0 +1,25 @@ +{ + "modelGroups": [ + { + "modelWeight": 100, + "currency": "NZD", + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml index 7d3c90274ca..ef28a3481be 100644 --- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml +++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml @@ -120,6 +120,12 @@ accounts: hook-sequence: - module-code: sample-it-module hook-impl-code: rejecting-processed-bidder-response + - id: 12001 + auction: + price-floors: + fetch: + url: http://localhost:8090/floors-provider + enabled: true domains: - rubiconproject.com - www.rubiconproject.com diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index d9f05a99a54..825bcc910e3 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -288,6 +288,8 @@ adapters.yieldone.enabled=true adapters.yieldone.endpoint=http://localhost:8090/yieldone-exchange adapters.zeroclickfraud.enabled=true adapters.zeroclickfraud.endpoint=http://{{Host}}/zeroclickfraud-exchange?sid={{SourceId}} +adapters.aax.enabled=true +adapters.aax.endpoint=http://localhost:8090/aax-exchange http-client.circuit-breaker.enabled=true http-client.circuit-breaker.idle-expire-hours=24 http-client.circuit-breaker.opening-threshold=1