From 587c2fb7c57bcd079ce540ef7da5e39efca5c602 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 16 Dec 2024 09:52:28 +0100 Subject: [PATCH 1/4] Price Floors: Max Rules and Max Dimensions --- .../floors/BasicPriceFloorProcessor.java | 19 ++++- .../server/floors/PriceFloorFetcher.java | 11 +-- .../floors/PriceFloorRulesValidator.java | 31 +++++-- .../floors/PriceFloorsConfigResolver.java | 25 +++++- .../model/AccountPriceFloorsConfig.java | 6 ++ .../model/AccountPriceFloorsFetchConfig.java | 3 + .../floors/BasicPriceFloorProcessorTest.java | 82 ++++++++++++++++--- .../server/floors/PriceFloorFetcherTest.java | 36 +++++++- .../floors/PriceFloorRulesValidatorTest.java | 61 +++++++++++--- .../floors/PriceFloorsConfigResolverTest.java | 26 ++++++ 10 files changed, 257 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index e2983875386..595df616bb6 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -135,12 +135,25 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li if (requestFloors != null) { try { - PriceFloorRulesValidator.validateRules(requestFloors, Integer.MAX_VALUE); + final Optional priceFloorsConfig = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceFloors); + + final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules) + .orElse(null); + final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDimensions) + .orElse(null); + + PriceFloorRulesValidator.validateRules( + requestFloors, + PriceFloorsConfigResolver.resolveMaxValue(maxRules), + PriceFloorsConfigResolver.resolveMaxValue(maxDimensions)); + return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); } catch (PreBidException e) { - errors.add("Failed to parse price floors from request, with a reason : %s ".formatted(e.getMessage())); + errors.add("Failed to parse price floors from request, with a reason: %s".formatted(e.getMessage())); conditionalLogger.error( - "Failed to parse price floors from request with id: '%s', with a reason : %s " + "Failed to parse price floors from request with id: '%s', with a reason: %s" .formatted(bidRequest.getId(), e.getMessage()), 0.01d); } diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index 0195c344474..32a04da1def 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -176,7 +176,10 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon } final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId); - PriceFloorRulesValidator.validateRulesData(priceFloorData, resolveMaxRules(fetchConfig.getMaxRules())); + PriceFloorRulesValidator.validateRulesData( + priceFloorData, + PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()), + PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDimensions())); return ResponseCacheInfo.of(priceFloorData, FetchStatus.success, @@ -194,12 +197,6 @@ private PriceFloorData parsePriceFloorData(String body, String accountId) { return priceFloorData; } - 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) { final String cacheControlValue = httpClientResponse.getHeaders().get(HttpHeaders.CACHE_CONTROL); final Matcher cacheHeaderMatcher = StringUtils.isNotBlank(cacheControlValue) diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java index a5f97299340..028686f82d4 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -4,12 +4,16 @@ 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.PriceFloorField; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.floors.model.PriceFloorSchema; import java.math.BigDecimal; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; public class PriceFloorRulesValidator { @@ -23,7 +27,7 @@ public class PriceFloorRulesValidator { private PriceFloorRulesValidator() { } - public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules) { + public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules, Integer maxDimensions) { final Integer rootSkipRate = priceFloorRules.getSkipRate(); if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) { @@ -36,10 +40,10 @@ public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRul throw new PreBidException("Price floor floorMin must be positive float, but was " + floorMin); } - validateRulesData(priceFloorRules.getData(), maxRules); + validateRulesData(priceFloorRules.getData(), maxRules, maxDimensions); } - public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules) { + public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules, Integer maxDimensions) { if (priceFloorData == null) { throw new PreBidException("Price floor rules data must be present"); } @@ -64,10 +68,10 @@ public static void validateRulesData(PriceFloorData priceFloorData, Integer maxR priceFloorData.getModelGroups().stream() .filter(Objects::nonNull) - .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); + .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules, maxDimensions)); } - private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) { + private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules, Integer maxDimensions) { final Integer modelWeight = modelGroup.getModelWeight(); if (modelWeight != null && (modelWeight < MODEL_WEIGHT_MIN_VALUE || modelWeight > MODEL_WEIGHT_MAX_VALUE)) { @@ -95,8 +99,21 @@ private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer } if (maxRules != null && values.size() > maxRules) { - throw new PreBidException( - "Price floor rules number %s exceeded its maximum number %s".formatted(values.size(), maxRules)); + throw new PreBidException("Price floor rules number %s exceeded its maximum number %s" + .formatted(values.size(), maxRules)); + } + + final List fields = Optional.ofNullable(modelGroup.getSchema()) + .map(PriceFloorSchema::getFields) + .orElse(null); + + if (CollectionUtils.isEmpty(fields)) { + throw new PreBidException("Price floor dimensions can't be null or empty, but were " + fields); + } + + if (maxDimensions != null && fields.size() > maxDimensions) { + throw new PreBidException("Price floor schema dimensions %s exceeded its maximum number %s" + .formatted(fields.size(), maxDimensions)); } } } diff --git a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java index 73eb62cfe84..7d21528f803 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java @@ -23,13 +23,15 @@ public class PriceFloorsConfigResolver { private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private static final int MIN_MAX_AGE_SEC_VALUE = 600; + private static final int MAX_AGE_SEC_VALUE = Integer.MAX_VALUE; private static final int MIN_PERIODIC_SEC_VALUE = 300; private static final int MIN_TIMEOUT_MS_VALUE = 10; private static final int MAX_TIMEOUT_MS_VALUE = 10_000; private static final int MIN_RULES_VALUE = 0; - private static final int MIN_FILE_SIZE_VALUE = 0; - private static final int MAX_AGE_SEC_VALUE = Integer.MAX_VALUE; private static final int MAX_RULES_VALUE = Integer.MAX_VALUE; + private static final int MIN_DIMENSIONS_VALUE = 0; + private static final int MAX_DIMENSIONS_VALUE = 19; + private static final int MIN_FILE_SIZE_VALUE = 0; private static final int MAX_FILE_SIZE_VALUE = Integer.MAX_VALUE; private static final int MIN_ENFORCE_RATE_VALUE = 0; private static final int MAX_ENFORCE_RATE_VALUE = 100; @@ -71,6 +73,16 @@ private static void validatePriceFloorConfig(Account account) { throw new PreBidException(invalidPriceFloorsPropertyMessage("enforce-floors-rate", enforceRate)); } + final Long maxRules = floorsConfig.getMaxRules(); + if (maxRules != null && isNotInRange(maxRules, MIN_RULES_VALUE, MAX_RULES_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); + } + + final Long maxDimensions = floorsConfig.getMaxSchemaDimensions(); + if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions)); + } + final AccountPriceFloorsFetchConfig fetchConfig = ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getFetch); @@ -108,6 +120,11 @@ private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); } + final Long maxDimensions = fetchConfig.getMaxSchemaDimensions(); + if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions)); + } + final Long maxFileSize = fetchConfig.getMaxFileSizeKb(); if (maxFileSize != null && isNotInRange(maxFileSize, MIN_FILE_SIZE_VALUE, MAX_FILE_SIZE_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-file-size-kb", maxFileSize)); @@ -121,4 +138,8 @@ private static boolean isNotInRange(long number, long min, long max) { private static String invalidPriceFloorsPropertyMessage(String property, Object value) { return "Invalid price-floors property '%s', value passed: %s".formatted(property, value); } + + public static int resolveMaxValue(Long value) { + return value != null && !value.equals(0L) ? Math.toIntExact(value) : Integer.MAX_VALUE; + } } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java index 76be21ea70e..5217b4ecd8b 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java @@ -23,4 +23,10 @@ public class AccountPriceFloorsConfig { @JsonAlias("use-dynamic-data") Boolean useDynamicData; + + @JsonAlias("max-rules") + Long maxRules; + + @JsonAlias("max-schema-dims") + Long maxSchemaDimensions; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java index 2cb3854371a..7eab14fe3d0 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java @@ -21,6 +21,9 @@ public class AccountPriceFloorsFetchConfig { @JsonAlias("max-rules") Long maxRules; + @JsonAlias("max-schema-dims") + Long maxSchemaDimensions; + @JsonAlias("max-age-sec") Long maxAgeSec; diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 8cc02e64a4a..332f33d6623 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -11,13 +11,13 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.auction.model.AuctionContext; 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; import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.floors.model.PriceFloorSchema; import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; @@ -30,6 +30,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Collections.singletonList; @@ -40,6 +41,8 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.floors.model.PriceFloorField.siteDomain; +import static org.prebid.server.floors.model.PriceFloorField.size; @ExtendWith(MockitoExtension.class) public class BasicPriceFloorProcessorTest extends VertxTest { @@ -383,6 +386,70 @@ public void shouldUseFloorsFromRequestIfProviderFloorsMissing() { .location(PriceFloorLocation.request))); } + @Test + public void shouldTolerateUsingFloorsFromRequestWhenRulesNumberMoreThanMaxRulesNumber() { + // given + given(priceFloorFetcher.fetch(any())).willReturn(null); + final ArrayList errors = new ArrayList<>(); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), givenFloors(floors -> floors.data( + PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .values(Map.of("someKey", BigDecimal.ONE, "someKey2", BigDecimal.ONE)) + .schema(PriceFloorSchema.of("|", List.of(size, siteDomain))) + .build())) + .build()) + )), + givenAccount(floorConfigBuilder -> floorConfigBuilder.maxRules(1L)), + "bidder", + errors, + new ArrayList<>()); + + // then + assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder() + .enabled(true) + .skipped(false) + .location(PriceFloorLocation.noData) + .build()); + + assertThat(errors).containsOnly("Failed to parse price floors from request, with a reason: " + + "Price floor rules number 2 exceeded its maximum number 1"); + } + + @Test + public void shouldTolerateUsingFloorsFromRequestWhenDimensionsNumberMoreThanMaxDimensionsNumber() { + // given + given(priceFloorFetcher.fetch(any())).willReturn(null); + final ArrayList errors = new ArrayList<>(); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), givenFloors(floors -> floors.data( + PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE) + .schema(PriceFloorSchema.of("|", List.of(size, siteDomain))) + .build())) + .build()) + )), + givenAccount(floorConfigBuilder -> floorConfigBuilder.maxSchemaDimensions(1L)), + "bidder", + errors, + new ArrayList<>()); + + // then + assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder() + .enabled(true) + .skipped(false) + .location(PriceFloorLocation.noData) + .build()); + + assertThat(errors).containsOnly("Failed to parse price floors from request, with a reason: " + + "Price floor schema dimensions 2 exceeded its maximum number 1"); + } + @Test public void shouldTolerateMissingRequestAndProviderFloors() { // given @@ -641,14 +708,6 @@ public void shouldTolerateFloorResolvingError() { assertThat(errors).containsOnly("Cannot resolve bid floor, error: error"); } - private static AuctionContext givenAuctionContext(Account account, BidRequest bidRequest) { - return AuctionContext.builder() - .prebidErrors(new ArrayList<>()) - .account(account) - .bidRequest(bidRequest) - .build(); - } - private static Account givenAccount( UnaryOperator floorsConfigCustomizer) { @@ -681,6 +740,7 @@ private static PriceFloorRules givenFloors( .data(PriceFloorData.builder() .modelGroups(singletonList(PriceFloorModelGroup.builder() .value("someKey", BigDecimal.ONE) + .schema(PriceFloorSchema.of("|", List.of(size))) .build())) .build()) ).build(); @@ -692,6 +752,7 @@ private static PriceFloorData givenFloorData( return floorDataCustomizer.apply(PriceFloorData.builder() .modelGroups(singletonList(PriceFloorModelGroup.builder() .value("someKey", BigDecimal.ONE) + .schema(PriceFloorSchema.of("|", List.of(size))) .build()))).build(); } @@ -699,7 +760,8 @@ private static PriceFloorModelGroup givenModelGroup( UnaryOperator modelGroupCustomizer) { return modelGroupCustomizer.apply(PriceFloorModelGroup.builder() - .value("someKey", BigDecimal.ONE)) + .value("someKey", BigDecimal.ONE) + .schema(PriceFloorSchema.of("|", List.of(size)))) .build(); } diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java index 64a66c507c0..f37dec221e2 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java @@ -15,7 +15,6 @@ import org.prebid.server.execution.timeout.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; @@ -31,6 +30,7 @@ import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.math.BigDecimal; +import java.util.List; import java.util.concurrent.TimeoutException; import java.util.function.UnaryOperator; @@ -45,6 +45,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.prebid.server.floors.model.PriceFloorField.domain; +import static org.prebid.server.floors.model.PriceFloorField.mediaType; +import static org.prebid.server.floors.model.PriceFloorField.siteDomain; @ExtendWith(MockitoExtension.class) public class PriceFloorFetcherTest extends VertxTest { @@ -497,6 +500,34 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules verifyNoMoreInteractions(vertx); } + @Test + public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededDimensionsNumber() { + // given + given(httpClient.get(anyString(), anyLong(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, + MultiMap.caseInsensitiveMultiMap(), + jacksonMapper.encodeToString(PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .schema(PriceFloorSchema.of("|", List.of(siteDomain, domain))) + .build())) + .build())))); + + // when + final FetchResult firstInvocationResult = + priceFloorFetcher.fetch(givenAccount(account -> account.maxSchemaDimensions(1L))); + + // then + verify(httpClient).get(anyString(), anyLong(), anyLong()); + assertThat(firstInvocationResult.getRulesData()).isNull(); + assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); + verify(vertx).setTimer(eq(1200000L), any()); + verify(vertx).setTimer(eq(1500000L), any()); + final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); + assertThat(secondInvocationResult.getRulesData()).isNull(); + assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); + verifyNoMoreInteractions(vertx); + } + private Account givenAccount(UnaryOperator< AccountPriceFloorsFetchConfig.AccountPriceFloorsFetchConfigBuilder> configCustomizer) { @@ -516,6 +547,7 @@ private static AccountPriceFloorsFetchConfig givenFetchConfig( .enabled(true) .url("http://test.host.com") .maxRules(10L) + .maxSchemaDimensions(10L) .maxFileSizeKb(10L) .timeoutMs(1300L) .maxAgeSec(1500L) @@ -528,7 +560,7 @@ private PriceFloorData givenPriceFloorData() { .currency("USD") .modelGroups(singletonList(PriceFloorModelGroup.builder() .modelVersion("model version 1.0") - .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) + .schema(PriceFloorSchema.of("|", singletonList(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 index 5516bb5c9e1..f4ec32e1165 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java @@ -4,8 +4,10 @@ import org.prebid.server.VertxTest; 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.PriceFloorRules; +import org.prebid.server.floors.model.PriceFloorSchema; import java.math.BigDecimal; import java.util.Arrays; @@ -15,6 +17,7 @@ import java.util.function.UnaryOperator; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.prebid.server.floors.model.PriceFloorField.size; public class PriceFloorRulesValidatorTest extends VertxTest { @@ -24,7 +27,7 @@ public void validateShouldThrowExceptionOnInvalidRootSkipRateWhenPresent() { final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.skipRate(-1)); assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor root skipRate must be in range(0-100), but was -1"); } @@ -36,7 +39,7 @@ public void validateShouldThrowExceptionWhenFloorMinPresentAndLessThanZero() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor floorMin must be positive float, but was -1"); } @@ -47,7 +50,7 @@ public void validateShouldThrowExceptionWhenDataIsAbsent() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor rules data must be present"); } @@ -58,7 +61,7 @@ public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor data skipRate must be in range(0-100), but was -1"); } @@ -70,7 +73,7 @@ public void validateShouldThrowExceptionOnInvalidUseFetchDataRateWhenPresent() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor data useFetchDataRate must be in range(0-100), but was -1"); } @@ -82,7 +85,7 @@ public void validateShouldThrowExceptionOnAbsentDataModelGroups() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor rules should contain at least one model group"); } @@ -94,7 +97,7 @@ public void validateShouldThrowExceptionOnEmptyDataModelGroups() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor rules should contain at least one model group"); } @@ -106,7 +109,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupModelWeightWhenPr // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor modelGroup modelWeight must be in range(1-100), but was -1"); } @@ -118,7 +121,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupSkipRateWhenPrese // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor modelGroup skipRate must be in range(0-100), but was -1"); } @@ -130,7 +133,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupDefaultFloorWhenP // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor modelGroup default must be positive float, but was -1"); } @@ -142,7 +145,7 @@ public void validateShouldThrowExceptionOnEmptyModelGroupValues() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) .withMessage("Price floor rules values can't be null or empty, but were {}"); } @@ -160,13 +163,46 @@ public void validateShouldThrowExceptionWhenModelGroupValuesSizeGreaterThanMaxRu // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, maxRules)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, maxRules, 100)) .withMessage( "Price floor rules number %s exceeded its maximum number %s", modelGroupValues.size(), maxRules); } + @Test + public void validateShouldThrowExceptionOnEmptyModelGroupFields() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.schema(PriceFloorSchema.of("|", Collections.emptyList()))); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100)) + .withMessage("Price floor dimensions can't be null or empty, but were []"); + } + + @Test + public void validateShouldThrowExceptionWhenModelGroupSchemaDimensionsSizeGreaterThanMaxDimensions() { + // given + final List modelGroupSchemaFields = List.of( + size, + PriceFloorField.bundle); + + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.schema(PriceFloorSchema.of("|", modelGroupSchemaFields))); + + final int maxDimensions = modelGroupSchemaFields.size() - 1; + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, maxDimensions)) + .withMessage( + "Price floor schema dimensions %s exceeded its maximum number %s", + modelGroupSchemaFields.size(), + maxDimensions); + } + private static PriceFloorRules givenPriceFloorRulesWithDataModelGroups( UnaryOperator... modelGroupBuilders) { @@ -175,6 +211,7 @@ private static PriceFloorRules givenPriceFloorRulesWithDataModelGroups( .modelWeight(10) .skipRate(10) .defaultFloor(BigDecimal.TEN) + .schema(PriceFloorSchema.of("|", List.of(size))) .values(Map.of("value", BigDecimal.TEN)); final List modelGroups = Arrays.stream(modelGroupBuilders) diff --git a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java index 0d3864395e6..2b3fe2ba0f8 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java @@ -188,6 +188,32 @@ public void resolveShouldReturnGivenAccountIfMaxRulesMoreThanMaximumValue() { verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); } + @Test + public void resolveShouldReturnGivenAccountIfMaxDimensionsLessThanMinimumValue() { + // given + final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDimensions(-1L)); + + // when + final Account actualAccount = target.resolve(givenAccount, defaultPriceConfig()); + + // then + assertThat(actualAccount).isEqualTo(fallbackAccount()); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void resolveShouldReturnGivenAccountIfMaxDimensionsMoreThanMaximumValue() { + // given + final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDimensions(20L)); + + // when + final Account actualAccount = target.resolve(givenAccount, defaultPriceConfig()); + + // then + assertThat(actualAccount).isEqualTo(fallbackAccount()); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + @Test public void resolveShouldReturnGivenAccountIfMaxFileSizeLessThanMinimumValue() { // given From 486bf699b332bcf3f48eeab2004d5458c9617add Mon Sep 17 00:00:00 2001 From: antonbabak Date: Thu, 19 Dec 2024 13:37:56 +0100 Subject: [PATCH 2/4] Rename a property --- .../org/prebid/server/floors/BasicPriceFloorProcessor.java | 2 +- src/main/java/org/prebid/server/floors/PriceFloorFetcher.java | 2 +- .../org/prebid/server/floors/PriceFloorsConfigResolver.java | 4 ++-- .../server/settings/model/AccountPriceFloorsConfig.java | 2 +- .../server/settings/model/AccountPriceFloorsFetchConfig.java | 2 +- .../prebid/server/floors/BasicPriceFloorProcessorTest.java | 2 +- .../java/org/prebid/server/floors/PriceFloorFetcherTest.java | 4 ++-- .../prebid/server/floors/PriceFloorsConfigResolverTest.java | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index 595df616bb6..91559480537 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -141,7 +141,7 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules) .orElse(null); - final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDimensions) + final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims) .orElse(null); PriceFloorRulesValidator.validateRules( diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index 32a04da1def..b7d4ac4185f 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -179,7 +179,7 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon PriceFloorRulesValidator.validateRulesData( priceFloorData, PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()), - PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDimensions())); + PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDims())); return ResponseCacheInfo.of(priceFloorData, FetchStatus.success, diff --git a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java index 7d21528f803..14834e24b7c 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java @@ -78,7 +78,7 @@ private static void validatePriceFloorConfig(Account account) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); } - final Long maxDimensions = floorsConfig.getMaxSchemaDimensions(); + final Long maxDimensions = floorsConfig.getMaxSchemaDims(); if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions)); } @@ -120,7 +120,7 @@ private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); } - final Long maxDimensions = fetchConfig.getMaxSchemaDimensions(); + final Long maxDimensions = fetchConfig.getMaxSchemaDims(); if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions)); } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java index 5217b4ecd8b..9acebb1c427 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java @@ -28,5 +28,5 @@ public class AccountPriceFloorsConfig { Long maxRules; @JsonAlias("max-schema-dims") - Long maxSchemaDimensions; + Long maxSchemaDims; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java index 7eab14fe3d0..42824b410e2 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java @@ -22,7 +22,7 @@ public class AccountPriceFloorsFetchConfig { Long maxRules; @JsonAlias("max-schema-dims") - Long maxSchemaDimensions; + Long maxSchemaDims; @JsonAlias("max-age-sec") Long maxAgeSec; diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 332f33d6623..78ce82dd3fd 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -434,7 +434,7 @@ public void shouldTolerateUsingFloorsFromRequestWhenDimensionsNumberMoreThanMaxD .build())) .build()) )), - givenAccount(floorConfigBuilder -> floorConfigBuilder.maxSchemaDimensions(1L)), + givenAccount(floorConfigBuilder -> floorConfigBuilder.maxSchemaDims(1L)), "bidder", errors, new ArrayList<>()); diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java index f37dec221e2..4fe2e31180b 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java @@ -514,7 +514,7 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededDimen // when final FetchResult firstInvocationResult = - priceFloorFetcher.fetch(givenAccount(account -> account.maxSchemaDimensions(1L))); + priceFloorFetcher.fetch(givenAccount(account -> account.maxSchemaDims(1L))); // then verify(httpClient).get(anyString(), anyLong(), anyLong()); @@ -547,7 +547,7 @@ private static AccountPriceFloorsFetchConfig givenFetchConfig( .enabled(true) .url("http://test.host.com") .maxRules(10L) - .maxSchemaDimensions(10L) + .maxSchemaDims(10L) .maxFileSizeKb(10L) .timeoutMs(1300L) .maxAgeSec(1500L) diff --git a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java index 2b3fe2ba0f8..9f975335cd9 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java @@ -191,7 +191,7 @@ public void resolveShouldReturnGivenAccountIfMaxRulesMoreThanMaximumValue() { @Test public void resolveShouldReturnGivenAccountIfMaxDimensionsLessThanMinimumValue() { // given - final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDimensions(-1L)); + final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDims(-1L)); // when final Account actualAccount = target.resolve(givenAccount, defaultPriceConfig()); @@ -204,7 +204,7 @@ public void resolveShouldReturnGivenAccountIfMaxDimensionsLessThanMinimumValue() @Test public void resolveShouldReturnGivenAccountIfMaxDimensionsMoreThanMaximumValue() { // given - final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDimensions(20L)); + final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDims(20L)); // when final Account actualAccount = target.resolve(givenAccount, defaultPriceConfig()); From 7b9fdd41245cafda02ed979fb98f1ac8ac3e0e91 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 7 Jan 2025 09:18:36 +0100 Subject: [PATCH 3/4] Add default-account config --- src/main/resources/application.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index eb73cc16478..31efd30851b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -174,6 +174,7 @@ settings: "enabled": false, "timeout-ms": 5000, "max-rules": 0, + "max-schema-dims": 5, "max-file-size-kb": 200, "max-age-sec": 86400, "period-sec": 3600 @@ -181,7 +182,9 @@ settings: "enforce-floors-rate": 100, "adjust-for-bid-adjustment": true, "enforce-deal-floors": true, - "use-dynamic-data": true + "use-dynamic-data": true, + "max-rules": 100, + "max-schema-dims": 3 } } } From 4097c127e861cc9a5033332e28beecee31ce036e Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:54:43 +0200 Subject: [PATCH 4/4] Test: Price floors max rules dimensions (#3646) * Add functional tests for price floors max rules dimensions --- .../config/AccountPriceFloorsConfig.groovy | 6 + .../model/config/PriceFloorsFetch.groovy | 3 + .../request/auction/ExtPrebidFloors.groovy | 1 + .../pricefloors/PriceFloorsBaseSpec.groovy | 7 +- .../PriceFloorsFetchingSpec.groovy | 121 ++++++- .../pricefloors/PriceFloorsRulesSpec.groovy | 6 +- .../PriceFloorsSignalingSpec.groovy | 321 ++++++++++++++++++ 7 files changed, 441 insertions(+), 24 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy index 28f908aba44..c83c69280fa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy @@ -15,6 +15,8 @@ class AccountPriceFloorsConfig { Boolean adjustForBidAdjustment Boolean enforceDealFloors Boolean useDynamicData + Long maxRules + Long maxSchemaDims @JsonProperty("enforce_floors_rate") Integer enforceFloorsRateSnakeCase @@ -24,4 +26,8 @@ class AccountPriceFloorsConfig { Boolean enforceDealFloorsSnakeCase @JsonProperty("use_dynamic_data") Boolean useDynamicDataSnakeCase + @JsonProperty("max_rules") + Long maxRulesSnakeCase + @JsonProperty("max_schema_dims") + Long maxSchemaDimsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy index 1501f2e1366..89f32a951a4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy @@ -26,4 +26,7 @@ class PriceFloorsFetch { Integer periodSec @JsonProperty("period_sec") Integer periodSecSnakeCase + Integer maxSchemaDims + @JsonProperty("max_schema_dims") + Integer maxSchemaDimsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy index cb5abf3ff87..c0c80038f5c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy @@ -20,6 +20,7 @@ class ExtPrebidFloors { ExtPrebidPriceFloorEnforcement enforcement Integer skipRate PriceFloorData data + Long maxSchemaDims static ExtPrebidFloors getExtPrebidFloors() { new ExtPrebidFloors(floorMin: FLOOR_MIN, 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 fba9a5b44d5..8b3f5d936bd 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 @@ -57,13 +57,16 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { maxRules: 0, maxFileSizeKb: 200, maxAgeSec: 86400, - periodSec: 3600) + periodSec: 3600, + maxSchemaDims: 5) def floors = new AccountPriceFloorsConfig(enabled: true, fetch: fetch, enforceFloorsRate: 100, enforceDealFloors: true, adjustForBidAdjustment: true, - useDynamicData: true) + useDynamicData: true, + maxRules: 0, + maxSchemaDims: 3) new AccountConfig(auction: new AccountAuctionConfig(priceFloors: floors)) } 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 f282d69f600..55180fe60d4 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 @@ -1299,8 +1299,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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"() { @@ -1327,8 +1327,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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"() { @@ -1355,8 +1355,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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"() { @@ -1387,8 +1387,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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] } @@ -1426,8 +1426,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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] @@ -1466,8 +1466,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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 << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] @@ -1506,8 +1506,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["Failed to parse price floors from request, with a reason: Price floor data skipRate " + + "must be in range(0-100), but was $invalidSkipRate"] where: invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] @@ -1546,8 +1546,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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 << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] @@ -1582,8 +1582,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 "] + ["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"() { @@ -2046,6 +2046,91 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } } + def "PBS should validate fetch.max-schema-dims from account config and not reject entire auction"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, maxSchemaDims in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.fetch.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + null | PBSUtils.randomNegativeNumber + null | PBSUtils.getRandomNumber(20) + PBSUtils.randomNegativeNumber | null + PBSUtils.getRandomNumber(20) | null + } + + def "PBS should validate price-floor.max-rules from account config and not reject entire auction"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, maxRules in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.maxRules = maxRules + config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + maxRules | maxRulesSnakeCase + null | PBSUtils.randomNegativeNumber + PBSUtils.randomNegativeNumber | null + } + + def "PBS should validate price-floor.max-schema-dims from account config and not reject entire auction"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, maxSchemaDims in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + null | PBSUtils.randomNegativeNumber + null | PBSUtils.getRandomNumber(20) + PBSUtils.randomNegativeNumber | null + PBSUtils.getRandomNumber(20) | null + } + static int convertKilobyteSizeToByte(int kilobyteSize) { kilobyteSize * 1024 } 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 478248d3f46..d10f6b07b1b 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 @@ -277,10 +277,8 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { where: bidRequest | bothFloorValue | bannerFloorValue | videoFloorValue - bidRequestWithMultipleMediaTypes | 0.6 | PBSUtils.randomFloorValue | - PBSUtils.randomFloorValue - BidRequest.defaultBidRequest | PBSUtils.randomFloorValue | 0.6 | - PBSUtils.randomFloorValue + bidRequestWithMultipleMediaTypes | 0.6 | PBSUtils.randomFloorValue | PBSUtils.randomFloorValue + BidRequest.defaultBidRequest | PBSUtils.randomFloorValue | 0.6 | PBSUtils.randomFloorValue BidRequest.defaultVideoRequest | PBSUtils.randomFloorValue | PBSUtils.randomFloorValue | 0.6 } 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 737aed289e4..f1385b1649d 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 @@ -18,6 +18,8 @@ import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.MediaType 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.bidder.BidderName.GENERIC @@ -27,9 +29,14 @@ import static org.prebid.server.functional.model.pricefloors.MediaType.VIDEO import static org.prebid.server.functional.model.pricefloors.PriceFloorField.MEDIA_TYPE import static org.prebid.server.functional.model.pricefloors.PriceFloorField.SITE_DOMAIN import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { + private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } + private static final int MAX_SCHEMA_DIMENSIONS_SIZE = 1 + private static final int MAX_RULES_SIZE = 1 + 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 { @@ -511,4 +518,318 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert bidderRequest.imp.first().bidFloor == bannerFloorValue assert bidderRequest.imp.last().bidFloor == videoFloorValue } + + def "PBS shouldn't emit errors when request schema.fields than floor-config.max-schema-dims"() { + given: "Bid request with schema 2 fields" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.maxSchemaDims = PBSUtils.getRandomNumber(2) + } + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't log a errors" + assert !response.ext?.errors + } + + def "PBS should emit errors when request has more rules than price-floor.max-rules"() { + given: "BidRequest with 2 rules" + def requestFloorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups[0].values = + [(rule) : requestFloorValue + 0.1, + (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue] + } + + and: "Account with maxRules in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.maxRules = maxRules + config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidResponse.seatbid.first().bid.first().price = requestFloorValue + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a errors" + 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 number ${getRuleSize(bidRequest)} exceeded its maximum number ${MAX_RULES_SIZE}"] + + where: + maxRules | maxRulesSnakeCase + MAX_RULES_SIZE | null + null | MAX_RULES_SIZE + } + + def "PBS should emit errors when request has more schema.fields than floor-config.max-schema-dims"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a errors" + 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 schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"] + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + MAX_SCHEMA_DIMENSIONS_SIZE | null + null | MAX_SCHEMA_DIMENSIONS_SIZE + } + + def "PBS should emit errors when request has more schema.fields than default-account.max-schema-dims"() { + given: "Floor config with default account" + def accountConfig = getDefaultAccountConfigSettings().tap { + auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + } + def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled" : "true", + "settings.default-account-config": encode(accountConfig)] + + and: "Prebid server with floor config" + def floorsPbsService = pbsServiceFactory.getService(pbsFloorConfig) + + and: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.maxSchemaDims = PBSUtils.randomNegativeNumber + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a errors" + 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 schema dimensions ${getSchemaSize(bidRequest)} " + + "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"] + + and: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsFloorConfig) + } + + def "PBS should emit errors when request has more schema.fields than default-account.fetch.max-schema-dims"() { + given: "Test start time" + def startTime = Instant.now() + + and: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Floor config with default account" + def accountConfig = getDefaultAccountConfigSettings().tap { + auction.priceFloors.fetch.enabled = true + auction.priceFloors.fetch.url = BASIC_FETCH_URL + bidRequest.site.publisher.id + auction.priceFloors.fetch.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + auction.priceFloors.maxSchemaDims = null + } + def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled" : "true", + "settings.default-account-config": encode(accountConfig)] + + and: "Prebid server with floor config" + def floorsPbsService = pbsServiceFactory.getService(pbsFloorConfig) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.maxSchemaDims = PBSUtils.randomNegativeNumber + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor schema dimensions ${getSchemaSize(bidRequest)} " + + "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}") + + and: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsFloorConfig) + } + + def "PBS should emit errors when request has more schema.fields than fetch.max-schema-dims"() { + given: "Default BidRequest with floorMin" + def bidRequest = bidRequestWithFloors + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a errors" + 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 schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"] + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + MAX_SCHEMA_DIMENSIONS_SIZE | null + null | MAX_SCHEMA_DIMENSIONS_SIZE + } + + def "PBS should fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def floorSchemaFilesSize = getSchemaSize(bidRequest) + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + config.auction.priceFloors.fetch.maxSchemaDims = floorSchemaFilesSize + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a errors" + 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 schema dimensions ${floorSchemaFilesSize} " + + "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"] + } + + def "PBS shouldn't fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.maxSchemaDims = getSchemaSize(bidRequest) + config.auction.priceFloors.fetch.maxSchemaDims = getSchemaSize(bidRequest) - 1 + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't log a errors" + assert !response.ext?.errors + } + + def "PBS should emit errors when stored request has more rules than price-floor.max-rules for amp request"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request with 2 rules " + def requestFloorValue = PBSUtils.randomFloorValue + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors + ext.prebid.floors.data.modelGroups[0].values = + [(rule) : requestFloorValue + 0.1, + (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue] + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Account with maxRules in the DB" + def account = getAccountWithEnabledFetch(ampRequest.account as String).tap { + config.auction.priceFloors.maxRules = maxRules + config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest) + bidResponse.seatbid.first().bid.first().price = requestFloorValue + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = floorsPbsService.sendAmpRequest(ampRequest) + + then: "PBS should log a errors" + 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 number ${getRuleSize(ampStoredRequest)} " + + "exceeded its maximum number ${MAX_RULES_SIZE}"] + + where: + maxRules | maxRulesSnakeCase + MAX_RULES_SIZE | null + null | MAX_RULES_SIZE + } + + private static int getSchemaSize(BidRequest bidRequest) { + bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].schema.fields.size() + } + + private static int getRuleSize(BidRequest bidRequest) { + bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].values.size() + } }