diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AuctionEnvironment.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AuctionEnvironment.groovy index f880b7bc634..649c539e794 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/AuctionEnvironment.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AuctionEnvironment.groovy @@ -1,7 +1,6 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonValue -import org.prebid.server.functional.util.PBSUtils enum AuctionEnvironment { diff --git a/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy index a1ab76389aa..f47efd346f8 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy @@ -5,8 +5,13 @@ import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AlternateBidderCodes import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.auction.Amx import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse @@ -269,7 +274,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == AMX.value + assert seatNonBid.seat == bidderName.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -324,7 +329,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == ALIAS.value + assert seatNonBid.seat == bidderName.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -782,7 +787,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == AMX.value + assert seatNonBid.seat == UNKNOWN.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -838,7 +843,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == AMX.value + assert seatNonBid.seat == UNKNOWN.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1182,7 +1187,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == AMX.value + assert seatNonBid.seat == requestedAllowedBidderCode.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1237,7 +1242,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == AMX.value + assert seatNonBid.seat == requestedAllowedBidderCode.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1298,7 +1303,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == AMX.value + assert seatNonBid.seat == allowedBidderCodes.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1572,6 +1577,228 @@ class AlternateBidderCodeSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } + def "PBS should populate two seat bid when different bidder response with same seat"() { + given: "Default bid request with amx and generic bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.generic = new Generic() + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat.sort() == [GENERIC, GENERIC].sort() + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten().sort() == [AMX, GENERIC].sort() + + and: "Response should contain bidder generic targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Response shouldn't contain repose millis with amx bidder" + assert !response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + } + + def "PBS should return two seat when same bidder response with different bidder code"() { + given: "Default bid request with amx and generic bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp.add(Imp.getDefaultImpression()) + imp[1].ext.prebid.bidder.amx = new Amx() + imp[1].ext.prebid.bidder.generic = null + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC, AMX])] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + it.seatbid[0].bid[1].ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX, AMX] + + and: "Response should contain bidder amx targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: 'Response targeting should contain generic' + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + } + + def "PBS should populate seat bid from stored bid response when stored bid response and alternate bidder code specified"() { + given: "Default bid request with amx bidder" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: AMX)] + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat == [AMX] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder generic targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request shouldn't be called due to storedBidResponse" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + } + + def "PBS auction allow bidder code when imp stored request and allowed bidder code present"() { + given: "Default bid request" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Requesting PBS auction" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat.sort() == [GENERIC].sort() + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder generic targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be called" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + } + private static Account getAccountWithAlternateBidderCode(BidRequest bidRequest) { new Account().tap { it.uuid = bidRequest.accountId diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 43b7a09777a..8effa51b231 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AlternateBidderCodes @@ -31,6 +32,7 @@ import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.GBP import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.AMX import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -1152,6 +1154,45 @@ class BidAdjustmentSpec extends BaseSpec { bidAdjustmentFactor << [0.9, 1.1] } + def "PBS should prefer bid price adjustment based on media type and alternate bidder code when request has per-media-type bid adjustment factors with soft alias"() { + given: "Default bid request with bid adjustment" + def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { + ext.prebid.aliases = [(ALIAS.value): AMX] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = null + imp[0].ext.prebid.bidder.alias = new Generic() + ext.prebid.tap { + bidAdjustmentFactors = new BidAdjustmentFactors().tap { + adjustments = [(GENERIC): randomDecimal] + mediaTypes = [(BANNER): [(GENERIC): bidAdjustmentFactor]] + } + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + } + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * + bidAdjustmentFactor + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + where: + bidAdjustmentFactor << [0.9, 1.1] + } + private static Map getExternalCurrencyConverterConfig() { ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, "currency-converter.external-rates.enabled" : "true", 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 20be3434042..8e6d7f1a57d 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 @@ -26,9 +26,10 @@ import java.time.Instant import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.bidder.BidderName.OPENX_ALIAS import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD -import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.* import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.DEVICE_ORCHESTRATED import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.NOT_SUPPORTED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.SERVER_ORCHESTRATED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.UNKNOWN import static org.prebid.server.functional.model.request.auction.PaaFormat.IAB import static org.prebid.server.functional.model.request.auction.PaaFormat.ORIGINAL import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID @@ -199,7 +200,7 @@ class OpenxSpec extends BaseSpec { assert interestGroupAuctionSeller.impId == impId assert interestGroupAuctionSeller.config assert interestGroupAuctionSeller.ext.bidder == bidResponse.seatbid[0].seat.value - assert interestGroupAuctionSeller.ext.adapter == OPENX.value + assert interestGroupAuctionSeller.ext.adapter == bidResponse.seatbid[0].seat.value } def "PBS shouldn't populate fledge config when bid response didn't return fledge config"() { @@ -307,7 +308,7 @@ class OpenxSpec extends BaseSpec { assert auctionConfigs?.size() == 1 assert auctionConfigs[0].impId == impId assert auctionConfigs[0].bidder == OPENX_ALIAS.value - assert auctionConfigs[0].adapter == OPENX.value + assert auctionConfigs[0].adapter == OPENX_ALIAS.value assert auctionConfigs[0].config == fledgeConfig and: "PBS response shouldn't contain igi config" @@ -350,7 +351,13 @@ class OpenxSpec extends BaseSpec { assert interestGroupAuctionSeller.impId == impId assert interestGroupAuctionSeller.config == fledgeConfig assert interestGroupAuctionSeller.ext.bidder == OPENX_ALIAS.value - assert interestGroupAuctionSeller.ext.adapter == OPENX.value + assert interestGroupAuctionSeller.ext.adapter == OPENX_ALIAS.value + + and: "Response should contain seat" + assert response.seatbid[0].seat == OPENX_ALIAS + + and: "Response should contain seat" + assert response.seatbid[0].bid[0].ext.prebid.meta.adapterCode == OPENX_ALIAS and: "PBS response shouldn't contain igi config" assert !response.ext?.interestGroupAuctionIntent?[0].interestGroupAuctionBuyer 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 e493bbb7df9..ee861a24587 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 @@ -4,6 +4,8 @@ import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.bidderspecific.BidderRequest +import org.prebid.server.functional.model.config.AlternateBidderCodes +import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType @@ -11,6 +13,7 @@ import org.prebid.server.functional.model.pricefloors.ModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule +import org.prebid.server.functional.model.request.auction.Amx import org.prebid.server.functional.model.request.auction.Banner import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device @@ -22,11 +25,13 @@ import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.ImpExtContextData import org.prebid.server.functional.model.request.auction.ImpExtContextDataAdServer import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.AMX import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.pricefloors.Country.USA @@ -1042,7 +1047,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Bidder request should contain proper bid floor value" def bidderRequests = bidder.getBidderRequests(bidRequest.id) - def impIdToBidderCallImp = impIdToBidderCallImp(bidderRequests) + def impIdToBidderCallImp = impIdToBidderCallImp(bidderRequests) assert impIdToBidderCallImp[bidRequest.imp[0].id].bidFloor == genericBidFloorRuleValue assert impIdToBidderCallImp[bidRequest.imp[1].id].bidFloor == openxBidFloorRuleValue @@ -1145,6 +1150,65 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { PBSUtils.randomFloorValue | PBSUtils.randomFloorValue } + def "PBS should populate seatNonBid when bid rejected due to floor and alternate bidder code specified"() { + given: "PBS config with floors config" + def config = FLOORS_CONFIG + + ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def pbsService = pbsServiceFactory.getService(config) + + and: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + generic = null + amx = new Amx() + } + ext.prebid.tap { + floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(enforcePbs: true)) + returnAllBidStatus = true + alternateBidderCodes = new AlternateBidderCodes( + enabled: true, + bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) + } + } + + and: "Account with enabled fetch, fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + } + floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + + and: "PBS cache rules" + cacheFloorsProviderRules(bidRequest, floorValue, pbsService, AMX) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.first().tap { + price = floorValue - 0.1 + ext = new BidExt(bidderCode: GENERIC) + } + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid" + def seatNonBids = response.ext.seatnonbid + assert seatNonBids.size() == 1 + + def seatNonBid = seatNonBids[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR + assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() + } + private static Map impIdToBidderCallImp(List bidderRequests) { bidderRequests.imp.flatten().collectEntries { [it.id, it] } as Map }