diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index 63ba2516ff4..a4fda13f829 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -40,6 +40,7 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { private static final int MIN_TIMEOUT = DEFAULT_TIMEOUT private static final int DEFAULT_TARGETING_PRECISION = 1 private static final String DEFAULT_CACHE_DIRECTORY = "/app/prebid-server/data" + protected static final String ALERT_GENERAL = "alerts.general" protected static final Map GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true", "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy index 579a8e16597..9dd17c31e0a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy @@ -63,7 +63,7 @@ class BidValidationSpec extends BaseSpec { and: "Bid validation metric value is incremented" def metrics = strictPrebidService.sendCollectedMetricsRequest() - assert metrics["alerts.general"] == 1 + assert metrics[ALERT_GENERAL] == 1 where: bidRequest << [BidRequest.getDefaultBidRequest(DistributionChannel.APP).tap { @@ -105,7 +105,7 @@ class BidValidationSpec extends BaseSpec { and: "Bid validation metric value is incremented" def metrics = softPrebidService.sendCollectedMetricsRequest() - assert metrics["alerts.general"] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "PBS log should contain message" def logs = softPrebidService.getLogsByTime(startTime) diff --git a/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy index 92ef175681e..ee3e808a56e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy @@ -146,7 +146,7 @@ class MetricsSpec extends BaseSpec { assert metrics["adapter.generic.requests.type.openrtb2-dooh" as String] == 1 and: "alert.general metric should be updated" - assert metrics["alerts.general" as String] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "Other channel types should not be populated" assert !metrics["account.${accountId}.requests.type.openrtb2-web" as String] @@ -175,7 +175,7 @@ class MetricsSpec extends BaseSpec { assert metrics["adapter.generic.requests.type.openrtb2-app" as String] == 1 and: "alert.general metric should be updated" - assert metrics["alerts.general" as String] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "Other channel types should not be populated" assert !metrics["account.${accountId}.requests.type.openrtb2-dooh" as String] diff --git a/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy index 8e6d7f1a57d..97a0015cea5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy @@ -444,7 +444,7 @@ class OpenxSpec extends BaseSpec { and: "Alert.general metric should be updated" def metrics = pbsService.sendCollectedMetricsRequest() - assert metrics["alerts.general" as String] == 1 + assert metrics[ALERT_GENERAL] == 1 } def "PBS shouldn't populate fledge or igi config when bidder respond with igb"() { 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 8b3f5d936bd..4e1cbbf349b 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 @@ -15,6 +15,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.BidRequestExt import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.ExtPrebidFloors +import org.prebid.server.functional.model.request.auction.FetchStatus import org.prebid.server.functional.model.request.auction.Prebid import org.prebid.server.functional.model.request.auction.Video import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse @@ -34,8 +35,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { public static final BigDecimal FLOOR_MIN = 0.5 public static final BigDecimal FLOOR_MAX = 2 - public static final Map FLOORS_CONFIG = ["price-floors.enabled" : "true", - "settings.default-account-config": encode(defaultAccountConfigSettings)] + public static final Map FLOORS_CONFIG = ["price-floors.enabled": "true"] protected static final String BASIC_FETCH_URL = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT protected static final int MAX_MODEL_WEIGHT = 100 @@ -121,8 +121,9 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { protected void cacheFloorsProviderRules(BidRequest bidRequest, PrebidServerService pbsService = floorsPbsService, - BidderName bidderName = BidderName.GENERIC) { - PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value]?.first?.ext?.prebid?.floors?.fetchStatus != INPROGRESS }, + BidderName bidderName = BidderName.GENERIC, + FetchStatus fetchStatus = INPROGRESS) { + PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value]?.first?.ext?.prebid?.floors?.fetchStatus != fetchStatus }, 5000, 1000) } 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 a897fbeff7c..699b9bdcda0 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 @@ -17,6 +17,7 @@ import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP @@ -43,8 +44,24 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { private static final int DEFAULT_FLOOR_VALUE_MIN = 0 private static final int FLOOR_MIN = 0 - private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure" + private static final String FETCHING_DISABLED_ERROR = 'Fetching is disabled' + private static final String PRICE_FLOOR_VALUES_MISSING = 'Price floor rules values can\'t be null or empty, but were null' + private static final String MODEL_WEIGHT_INVALID = "Price floor modelGroup modelWeight must be in range(1-100), but was %s" + private static final String SKIP_RATE_INVALID = "Price floor modelGroup skipRate must be in range(0-100), but was %s" + + private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } + private static final Closure URL_EMPTY_ERROR = { url -> "Failed to fetch price floor from provider for fetch.url '${url}'" } + private static final Closure URL_EMPTY_WARNING_MESSAGE = { url, message -> + "${URL_EMPTY_ERROR(url)}, with a reason: $message" + } + private static final Closure URL_EMPTY_ERROR_LOG = { bidRequest, message -> + "No price floor data for account ${bidRequest.accountId} and " + + "request ${bidRequest.id}, reason: ${URL_EMPTY_WARNING_MESSAGE("$BASIC_FETCH_URL$bidRequest.accountId", message)}" + } + private static final Closure WARNING_MESSAGE = { message -> + "$FETCHING_DISABLED_ERROR. Following parsing of request price floors is failed: $message" + } def "PBS should activate floors feature when price-floors.enabled = true in PBS config"() { given: "Pbs with PF configuration" @@ -121,7 +138,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def logs = floorsPbsService.getLogsByTime(startTime) def floorsLogs = getLogsByText(logs, bidRequest.accountId) assert floorsLogs.size() == 1 - assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.accountId") + assert floorsLogs.first().contains("No price floor data for account $bidRequest.accountId and request $bidRequest.id, " + + "reason: Malformed fetch.url 'null' passed") and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() @@ -545,7 +563,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest, floorsPbsService) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -554,14 +572,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def metrics = floorsPbsService.sendCollectedMetricsRequest() assert metrics[FETCH_FAILURE_METRIC] == 1 - then: "PBS should fetch data" + and: "PBS should fetch data" assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) + def message = "Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate" def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId") assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("reason : Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -645,7 +664,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, BAD_REQUEST_400) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest, floorsPbsService) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -657,12 +676,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Failed to request, provider respond with status 400" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 : Failed to request for " + - "account $accountId, provider respond with status 400") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -687,6 +705,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def invalidJson = "{{}}" floorsProvider.setResponse(accountId, invalidJson) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -697,12 +718,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Failed to parse price floor response, cause: DecodeException: Failed to decode" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 : Failed to parse price floor " + - "response for account $accountId, cause: DecodeException: Failed to decode") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -726,6 +746,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with invalid json" floorsProvider.setResponse(accountId, "") + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -736,12 +759,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Failed to parse price floor response, response body can not be empty" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 : Failed to parse price floor " + - "response for account $accountId, response body can not be empty" as String) + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -768,6 +790,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } floorsProvider.setResponse(accountId, floorsResponse) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -778,12 +803,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor rules should contain at least one model group" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 rules should contain " + - "at least one model group " as String) + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -810,6 +834,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } floorsProvider.setResponse(accountId, floorsResponse) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -821,11 +848,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 rules values can't " + - "be null or empty, but were null" as String) + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, PRICE_FLOOR_VALUES_MISSING)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -855,6 +880,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } floorsProvider.setResponse(accountId, floorsResponse) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -865,12 +893,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor rules number 2 exceeded its maximum number $maxRules" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 rules number " + - "2 exceeded its maximum number $maxRules") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -901,6 +928,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with timeout" floorsProvider.setResponseWithTimeout(accountId) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, pbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -914,8 +944,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def logs = pbsService.getLogsByTime(startTime) def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$BASIC_FETCH_URL$accountId', " + - "account $accountId exceeded") + assert floorsLogs[0].contains("No price floor data for account $bidRequest.accountId and request $bidRequest.id, " + + "reason: Fetch price floor request timeout for fetch.url '$BASIC_FETCH_URL$accountId' exceeded") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -944,6 +974,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def responseSize = convertKilobyteSizeToByte(maxSize) + 75 floorsProvider.setResponse(accountId, floorsResponse, ["Content-Length": responseSize as String]) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -954,12 +987,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Response size $responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 : Response size " + - "$responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -1284,11 +1316,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor floorMin " + - "must be positive float, but was $invalidFloorMin"] + and: "Response should contain warning" + def message = "Price floor floorMin must be positive float, but was $invalidFloorMin" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] } def "PBS should validate rules from request when request doesn't contain modelGroups"() { @@ -1312,11 +1343,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor rules " + - "should contain at least one model group"] + and: "Response should contain warning" + def message = "Price floor rules should contain at least one model group" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] } def "PBS should validate rules from request when request doesn't contain values"() { @@ -1340,11 +1370,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor rules values " + - "can't be null or empty, but were null"] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(PRICE_FLOOR_VALUES_MISSING)] } def "PBS should validate rules from request when modelWeight from request is invalid"() { @@ -1372,11 +1400,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor modelGroup modelWeight " + - "must be in range(1-100), but was $invalidModelWeight"] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))] + where: invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] } @@ -1411,11 +1438,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor modelGroup modelWeight " + - "must be in range(1-100), but was $invalidModelWeight"] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))] where: invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] @@ -1451,11 +1476,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor root skipRate " + - "must be in range(0-100), but was $invalidSkipRate"] + and: "Response should contain warning" + def message = "Price floor root skipRate must be in range(0-100), but was $invalidSkipRate" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] where: invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] @@ -1491,11 +1515,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor data skipRate " + - "must be in range(0-100), but was $invalidSkipRate"] + and: "Response should contain warning" + def message = "Price floor data skipRate must be in range(0-100), but was $invalidSkipRate" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] where: invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] @@ -1531,11 +1554,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor modelGroup skipRate " + - "must be in range(0-100), but was $invalidSkipRate"] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(SKIP_RATE_INVALID.formatted(invalidSkipRate))] where: invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] @@ -1567,11 +1588,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason: Price floor modelGroup default " + - "must be positive float, but was $invalidDefaultFloorValue"] + and: "Response should contain warning" + def message = "Price floor modelGroup default must be positive float, but was $invalidDefaultFloorValue" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] } def "PBS should not invalidate previously good fetched data when floors provider return invalid data"() { @@ -1651,7 +1671,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1669,11 +1689,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 modelGroup modelWeight" + - " must be in range(1-100), but was $invalidModelWeight") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -1710,7 +1728,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1727,12 +1745,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor data skipRate must be in range(0-100), but was $invalidSkipRate" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 data skipRate" + - " must be in range(0-100), but was $invalidSkipRate") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -1769,7 +1786,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1787,11 +1804,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 modelGroup skipRate" + - " must be in range(0-100), but was $invalidSkipRate") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, SKIP_RATE_INVALID.formatted(invalidSkipRate))) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -1828,7 +1843,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1845,12 +1860,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor modelGroup default must be positive float, but was $invalidDefaultFloor" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 modelGroup default" + - " must be positive float, but was $invalidDefaultFloor") + assert floorsLogs[0].contains(URL_EMPTY_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() 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 ee861a24587..30acd9b4bdc 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 @@ -61,6 +61,7 @@ import static org.prebid.server.functional.model.request.auction.FetchStatus.ERR import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Prebid.Channel import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { @@ -217,10 +218,15 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { assert bidderRequest.ext?.prebid?.floors?.location == NO_DATA assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR - and: "PBS should not contain errors, warnings" - assert !response.ext?.warnings + and: "PBS should not contain errors" assert !response.ext?.errors + and: "PBS should log a warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message.first.contains("Cannot deserialize value of type " + + "`org.prebid.server.floors.model.PriceFloorField` " + + "from String \"bogus\": not one of the values accepted for Enum class") + and: "PBS should not reject the entire auction" assert !response.seatbid.isEmpty() } 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 72be2f0a0f3..1de08b44cc9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy @@ -1,5 +1,9 @@ package org.prebid.server.functional.tests.pricefloors +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountPriceFloorsConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.ModelGroup @@ -29,13 +33,30 @@ 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.request.auction.FetchStatus.INPROGRESS +import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { + // Required: Sync no longer logs "in progress"—only "none" or "error" statuses are recorded + private static final String FETCHING_DISABLED_ERROR = 'Fetching is disabled' private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } + private static final Closure WARNING_MESSAGE = { message -> + "$FETCHING_DISABLED_ERROR. Following parsing of request price floors is failed: $message" + } + private static final Closure ERROR_LOG = { bidRequest, message -> + "No price floor data for account ${bidRequest.accountId} and " + + "request ${bidRequest.id}, reason: ${WARNING_MESSAGE(message)}" + } private static final int MAX_SCHEMA_DIMENSIONS_SIZE = 1 private static final int MAX_RULES_SIZE = 1 + private static Instant startTime + + def setupSpec() { + startTime = Instant.now() + } def "PBS should skip signalling for request with rules when ext.prebid.floors.enabled = false in request"() { given: "Default BidRequest with disabled floors" @@ -280,7 +301,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) - then: "PBS should not log warning" + then: "PBS should not log warning or errors" assert !response.ext.warnings assert !response.ext.errors @@ -319,8 +340,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { when: "PBS processes amp request" def response = floorsPbsService.sendAmpRequest(ampRequest) - then: "PBS should not log warning" + then: "PBS should not log warning or errors" assert !response.ext.warnings + assert !response.ext.errors and: "Bidder request should contain bidFloor from the request" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) @@ -520,7 +542,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert bidderRequest.imp.last().bidFloor == videoFloorValue } - def "PBS shouldn't emit errors when request schema.fields than floor-config.max-schema-dims"() { + def "PBS shouldn't emit warning when request schema.fields equal to floor-config.max-schema-dims"() { given: "Bid request with schema 2 fields" def bidRequest = bidRequestWithFloors.tap { ext.prebid.floors.maxSchemaDims = PBSUtils.getRandomNumber(2) @@ -531,18 +553,19 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { 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 + then: "PBS should not add warning or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] } - def "PBS should emit errors when request has more rules than price-floor.max-rules"() { + def "PBS should emit warning when request has more rules than price-floor.max-rules"() { given: "BidRequest with 2 rules" def requestFloorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { @@ -551,9 +574,10 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue] } - and: "Account with maxRules in the DB" + and: "Account with maxRules and disabled fetching in the DB" def accountId = bidRequest.site.publisher.id def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.enabled = false config.auction.priceFloors.maxRules = maxRules config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase } @@ -564,14 +588,20 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { bidResponse.seatbid.first().bid.first().price = requestFloorValue bidder.setResponse(bidRequest.id, bidResponse) + and: "Flush metrics" + flushMetrics(floorsPbsService) + 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}"] + then: "PBS should log a warning" + def message = "Price floor rules number ${getRuleSize(bidRequest)} exceeded its maximum number ${MAX_RULES_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 where: maxRules | maxRulesSnakeCase @@ -579,30 +609,36 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { null | MAX_RULES_SIZE } - def "PBS should emit errors when request has more schema.fields than floor-config.max-schema-dims"() { + def "PBS should emit warning 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" + and: "Account with maxSchemaDims and disabled fetching in the DB" def accountId = bidRequest.site.publisher.id def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.enabled = false 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) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + and: "Flush metrics" + flushMetrics(floorsPbsService) 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}"] + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 where: maxSchemaDims | maxSchemaDimsSnakeCase @@ -610,7 +646,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { null | MAX_SCHEMA_DIMENSIONS_SIZE } - def "PBS should emit errors when request has more schema.fields than default-account.max-schema-dims"() { + def "PBS should emit warning 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 @@ -627,45 +663,46 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { 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) + and: "Flush metrics" + flushMetrics(floorsPbsService) 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}"] + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 - and: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + and: "Metrics should be updated" def metrics = floorsPbsService.sendCollectedMetricsRequest() assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + assert metrics[ALERT_GENERAL] == 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 "PBS should emit warning when request has more schema.fields than default-account.fetch.max-schema-dims"() { + given: "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.enabled = false auction.priceFloors.fetch.url = BASIC_FETCH_URL + bidRequest.site.publisher.id auction.priceFloors.fetch.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE - auction.priceFloors.maxSchemaDims = null + auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE } def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled" : "true", "settings.default-account-config": encode(accountConfig)] @@ -683,48 +720,59 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { } 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) + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Response should includer error warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] - then: "PBS should log a errors" + and: "PBS shouldn't log a errors" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) 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" + and: "Metrics should be updated" def metrics = floorsPbsService.sendCollectedMetricsRequest() assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + assert metrics[ALERT_GENERAL] == 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 "PBS should emit warning when request has more schema.fields than fetch.max-schema-dims"() { + given: "BidRequest with schema 2 fields" def bidRequest = bidRequestWithFloors and: "Account with disabled fetch in the DB" def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.enabled = false config.auction.priceFloors.maxSchemaDims = maxSchemaDims config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase } accountDao.save(account) + and: "Flush metrics" + flushMetrics(floorsPbsService) + 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}"] + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 where: maxSchemaDims | maxSchemaDimsSnakeCase @@ -736,28 +784,36 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { given: "BidRequest with schema 2 fields" def bidRequest = bidRequestWithFloors - and: "Account with maxSchemaDims in the DB" + and: "Account with maxSchemaDims and disabled fetching in the DB" def accountId = bidRequest.site.publisher.id def floorSchemaFilesSize = getSchemaSize(bidRequest) def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.enabled = false 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) + and: "Flush metrics" + flushMetrics(floorsPbsService) 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}"] + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${floorSchemaFilesSize} " + + "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 } def "PBS shouldn't fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() { @@ -772,22 +828,18 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { } 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 + then: "PBS shouldn't add warnings or errors" + assert !response.ext?.warnings } - def "PBS should emit errors when stored request has more rules than price-floor.max-rules for amp request"() { + def "PBS should emit warning 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 " + and: "Default stored request with 2 rules" def requestFloorValue = PBSUtils.randomFloorValue def ampStoredRequest = BidRequest.defaultStoredRequest.tap { ext.prebid.floors = ExtPrebidFloors.extPrebidFloors @@ -798,8 +850,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) - and: "Account with maxRules in the DB" + and: "Account with maxRules and disabled fetching in the DB" def account = getAccountWithEnabledFetch(ampRequest.account as String).tap { + config.auction.priceFloors.fetch.enabled = false config.auction.priceFloors.maxRules = maxRules config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase } @@ -810,15 +863,27 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { bidResponse.seatbid.first().bid.first().price = requestFloorValue bidder.setResponse(ampStoredRequest.id, bidResponse) + and: "Flush metrics" + flushMetrics(floorsPbsService) + 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}"] + then: "PBS should log a warning" + def message = "Price floor rules number ${getRuleSize(ampStoredRequest)} " + + "exceeded its maximum number ${MAX_RULES_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, "No price floor data for account $ampRequest.account and " + + "request $ampStoredRequest.id, reason: ${WARNING_MESSAGE(message)}") + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 where: maxRules | maxRulesSnakeCase @@ -826,6 +891,298 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { null | MAX_RULES_SIZE } + def "PBS shouldn't emit error in log and response when data is invalid and floors status is in progress"() { + given: "Default BidRequest with empty floors.data" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data = null + } + + and: "Account with enabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't includer error warning" + assert !bidResponse.ext?.warnings + + and: "PBS shouldn't log a errors" + def message = 'Price floor rules data must be present' + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert !floorsLogs.size() + + and: "PBS request on response object status should be in progress" + assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == INPROGRESS + + and: "PBS bidderRequest status should be in progress" + def bidderRequest = bidder.getBidderRequests(bidRequest.id) + assert bidderRequest.ext.prebid.floors.fetchStatus == [INPROGRESS] + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + } + + def "PBS should emit error in log and response when data is invalid and floors status not in progress"() { + given: "Default BidRequest with empty floors.data" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data = null + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Account with disabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = 'Price floor rules data must be present' + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "PBS request on response object status should be none" + assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == NONE + + and: "PBS request status shouldn't be in progress" + def bidderRequest = bidder.getBidderRequests(bidRequest.id) + assert bidderRequest.ext.prebid.floors.fetchStatus == [NONE] + + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS should emit error in log and response when floors skipRate is out of range"() { + given: "BidRequest with invalid skipRate" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.skipRate = requestSkipRate + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor data skipRate must be in range(0-100), but was $requestSkipRate" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestSkipRate << [PBSUtils.randomNegativeNumber, PBSUtils.getRandomNumber(100)] + } + + def "PBS should emit error in log and response when floors modelGroups is empty"() { + given: "BidRequest with empty modelGroups" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = requestModelGroups + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor rules should contain at least one model group" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestModelGroups << [null, []] + } + + def "PBS should emit error in log and response when modelGroup modelWeight is out of range"() { + given: "BidRequest with invalid modelWeight" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = [ + new ModelGroup(modelWeight: requestModelWeight) + ] + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor modelGroup modelWeight must be in range(1-100), but was $requestModelWeight" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestModelWeight << [PBSUtils.randomNegativeNumber, PBSUtils.getRandomNumber(100)] + } + + def "PBS should emit error in log and response when modelGroup skipRate is out of range"() { + given: "BidRequest with invalid modelGroup skipRate" + def requestModelGroupsSkipRate = PBSUtils.getRandomNumber(100) + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = [ + new ModelGroup(skipRate: requestModelGroupsSkipRate) + ] + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor modelGroup skipRate must be in range(0-100), but was $requestModelGroupsSkipRate" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log an errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS should emit error in log and response when modelGroup defaultFloor is negative"() { + given: "BidRequest with negative defaultFloor" + def requestModelGroupsSkipRate = PBSUtils.randomNegativeNumber + + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = [ + new ModelGroup(defaultFloor: requestModelGroupsSkipRate) + ] + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor modelGroup default must be positive float, but was $requestModelGroupsSkipRate" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, ERROR_LOG(bidRequest, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS should use default floors config when original account config is invalid"() { + given: "Bid request with empty floors" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with disabled price floors" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(priceFloors: new AccountPriceFloorsConfig(enabled: false))) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + then: "PBS shouldn't log warning or errors" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + } + + def "PBS should emit error in log and response when account have disabled dynamic data config"() { + given: "Bid request without floors" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with disabled dynamic data" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, SUCCESS) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == ['Using dynamic data is not allowed'] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, "No price floor data for account ${bidRequest.accountId} " + + "and request ${bidRequest.id}, reason: Using dynamic data is not allowed") + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + private static int getSchemaSize(BidRequest bidRequest) { bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].schema.fields.size() } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy index 771fac9bd84..717c9f32d5b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy @@ -394,7 +394,7 @@ class GdprAmpSpec extends PrivacyBaseSpec { and: "Alerts.general metrics should be populated" def metrics = privacyPbsService.sendCollectedMetricsRequest() - assert metrics["alerts.general"] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "Bidder should be called" assert bidder.getBidderRequest(ampStoredRequest.id) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 7302ad79aed..5fc0f5d7bca 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -342,7 +342,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { and: "Alerts.general metrics should be populated" def metrics = privacyPbsService.sendCollectedMetricsRequest() - assert metrics["alerts.general"] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "Bid response should contain seatBid" assert response.seatbid.size() == 1