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() + } }