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