diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 2dc5ff7c77b..b0a37977b4a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -37,6 +37,8 @@ class AccountAuctionConfig { BidAdjustment bidAdjustments BidRounding bidRounding Integer impressionLimit + @JsonProperty("secondarybidders") + List secondaryBidders @JsonProperty("price_granularity") PriceGranularityType priceGranularitySnakeCase diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy index 80f504a05ec..04d6535f8ad 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy @@ -17,6 +17,7 @@ enum ErrorType { OPENX("openx"), AMX("amx"), AMX_UPPER_CASE("AMX"), + OPENX_ALIAS("openxalias"), @JsonValue final String value diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy index 05d6fcfa3d7..99025431a76 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy @@ -12,6 +12,7 @@ import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.response.auction.BidResponse import org.testcontainers.containers.MockServerContainer +import static java.util.concurrent.TimeUnit.SECONDS import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 @@ -47,6 +48,13 @@ class Bidder extends NetworkScaffolding { : HttpResponse.notFoundResponse()} } + void setResponseWithDilay(Integer dilayTimeout = 5) { + mockServerClient.when(request().withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond {request -> request.withPath(endpoint) + ? response().withDelay(SECONDS, dilayTimeout).withStatusCode(OK_200.code()).withBody(getBodyByRequest(request)) + : HttpResponse.notFoundResponse()} + } + List getBidderRequests(String bidRequestId) { getRecordedRequestsBody(bidRequestId).collect { decode(it, BidderRequest) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy new file mode 100644 index 00000000000..1da90a2c1f5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy @@ -0,0 +1,338 @@ +package org.prebid.server.functional.tests + + +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.Bidder +import spock.lang.Shared + +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX_ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN + +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class SecondaryBidderSpec extends BaseSpec { + + private static final Map OPENX_CONFIG = [ + "adapters.${OPENX.value}.enabled" : "true", + "adapters.${OPENX.value}.endpoint": "$networkServiceContainer.rootUri/openx-auction".toString()] + + private static final Map OPENX_ALIAS_CONFIG = [ + "adapters.${OPENX.value}.aliases.${OPENX_ALIAS}.enabled" : "true", + "adapters.${OPENX.value}.aliases.${OPENX_ALIAS}.endpoint": "$networkServiceContainer.rootUri/openx-alias-auction".toString()] + + protected static final Bidder openXBidder = new Bidder(networkServiceContainer, "/openx-auction") + protected static final Bidder openXAliasBidder = new Bidder(networkServiceContainer, "/openx-alias-auction") + + @Shared + PrebidServerService pbsServiceWithOpenXAndIXBidder = pbsServiceFactory.getService(OPENX_CONFIG + OPENX_ALIAS_CONFIG) + + @Override + def cleanupSpec() { + pbsServiceFactory.removeContainer(OPENX_CONFIG + OPENX_ALIAS_CONFIG) + } + + def "PBS should proceed as default when secondaryBidders not define in config"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + it.auction = new AccountAuctionConfig(secondaryBidders: null) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + assert bidder.getBidderRequest(bidRequest.id) + + and: "PBS shouldn't contain errors, warnings and seat non bit" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + assert !bidResponse.ext.seatnonbid + } + + def "PBS should emit a warning when null in secondary bidders config"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + it.auction = new AccountAuctionConfig(secondaryBidders: [null]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + assert bidder.getBidderRequest(bidRequest.id) + + and: "PBS shouldn't contain errors, warnings and seat non bid" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + assert !bidResponse.ext.seatnonbid + } + + def "PBS should emit a warning when invalid bidder in secondary bidders config"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + it.auction = new AccountAuctionConfig(secondaryBidders: [UNKNOWN]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert bidderRequests.size() == 3 + + and: "PBS shouldn't contain errors, warnings and seat non bid" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + assert !bidResponse.ext.seatnonbid + } + + //todo: If every bidder in the auction is flagged as secondary, + // then the feature is ignored, and all bidders are considered 'primary'. + + def "PBS should thread all bidders as primary when all requested bidders in secondary bidders config"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + it.auction = new AccountAuctionConfig(secondaryBidders: [GENERIC, OPENX]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert bidderRequests.size() == 2 + + and: "PBS shouldn't contain errors, warnings and seat non bid" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + assert !bidResponse.ext.seatnonbid + } + + //todo: If a bidder is defined as secondary by the account-level config, + // PBS should not wait for that bidder to respond. i.e. + // when the last primary bidder responds, the auction is over and any secondary bidder that hasn't returned is considered timed out. + + def "PBS shouldn't wait on non prioritize bidder when primary bidder respond"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.imp[0].ext.prebid.bidder.tap { + it.openx = Openx.defaultOpenx + } + } + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + // it.auction = new AccountAuctionConfig(secondaryBidders: [OPENX]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Set up openx bidder response with delay" + openXBidder.setResponseWithDilay(5) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + def genericBidderRequests = bidder.getBidderRequests(bidRequest.id) + def openXBidderRequests = openXBidder.getBidderRequests(bidRequest.id) + assert genericBidderRequests.size() == 1 + assert openXBidderRequests.size() == 1 + + and: "PBs repose shouldn't contain response body from openX bidder" + assert !bidResponse?.ext?.debug?.httpcalls[OPENX]?.responseBody + + and: "PBS should contain error for openX due to timeout" + assert bidResponse.ext?.errors[ErrorType.OPENX] + + and: "PBs should respond with warning for openx" + assert bidResponse.ext?.warnings[ErrorType.OPENX].message == ["Secondary bidder timed out, auction proceeded"] + + and: "PBs should populate seatNonBid" + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED + } + + //todo: Aliases are treated separately. i.e. + // just because a biddercode is defined as secondary does not mean any aliases or root biddercodes are also secondary. + + def "PBS shouldn't treated alias bidder as secondary when root bidder code in secondary"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.imp[0].ext.prebid.bidder.tap { + it.openx = Openx.defaultOpenx + it.openxAlias = Openx.defaultOpenx + } + ext.prebid.aliases = [(OPENX_ALIAS.value): OPENX] + } + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + // it.auction = new AccountAuctionConfig(secondaryBidders: [OPENX]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Set up openx bidder response with delay" + openXBidder.setResponseWithDilay(5) + + and: "Set up openx alias bidder response" + openXAliasBidder.setResponse() + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + def genericBidderRequests = bidder.getBidderRequests(bidRequest.id) + def openXAliasBidderRequests = openXAliasBidder.getBidderRequests(bidRequest.id) + def openXBidderRequests = openXBidder.getBidderRequests(bidRequest.id) + assert genericBidderRequests.size() == 1 + assert openXBidderRequests.size() == 1 + assert openXAliasBidderRequests.size() == 1 + + and: "PBs repose shouldn't contain response body from openX bidder" + assert !bidResponse?.ext?.debug?.httpcalls[OPENX]?.responseBody + + and: "PBS should contain error for openX due to timeout" + assert bidResponse.ext?.errors[ErrorType.OPENX] + + and: "PBs should respond with warning for openx" + assert bidResponse.ext?.warnings[ErrorType.OPENX].message == ["Secondary bidder openx timed out, auction proceeded"] + + and: "PBs should populate seatNonBid" + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED + + cleanup: "Reset mock" + openXBidder.reset() + openXAliasBidder.reset() + } + + def "PBS shouldn't wait on secondary bidder when alias bidder respond with dilay"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.imp[0].ext.prebid.bidder.tap { + it.openx = Openx.defaultOpenx + it.openxAlias = Openx.defaultOpenx + } + ext.prebid.aliases = [(OPENX_ALIAS.value): OPENX] + } + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + // it.auction = new AccountAuctionConfig(secondaryBidders: [OPENX_ALIAS]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Set up openx bidder response with delay" + openXAliasBidder.setResponseWithDilay(5) + + and: "Set up openx alias bidder response" + openXBidder.setResponse() + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + def genericBidderRequests = bidder.getBidderRequests(bidRequest.id) + def openXAliasBidderRequests = openXAliasBidder.getBidderRequests(bidRequest.id) + def openXBidderRequests = openXBidder.getBidderRequests(bidRequest.id) + assert genericBidderRequests.size() == 1 + assert openXBidderRequests.size() == 1 + assert openXAliasBidderRequests.size() == 1 + + and: "PBs repose shouldn't contain response body from openX bidder" + assert !bidResponse?.ext?.debug?.httpcalls[OPENX_ALIAS]?.responseBody + + and: "PBS should contain error for openX due to timeout" + assert bidResponse.ext?.errors[ErrorType.OPENX_ALIAS] + + and: "PBs should respond with warning for openx" + assert bidResponse.ext?.warnings[ErrorType.OPENX_ALIAS].message == ["Secondary bidder opnex_alias timed out, auction proceeded"] + + and: "PBs should populate seatNonBid" + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX_ALIAS + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED + + cleanup: "Reset mock" + openXBidder.reset() + openXAliasBidder.reset() + } + + //todo: what if primary bidder will respond slowest than secondary bidder, usual flow of action? + + def "PBS should pass auction as usual when secondary bidder respond first and primary with dilay"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.imp[0].ext.prebid.bidder.tap { + it.openx = Openx.defaultOpenx + } + } + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + // it.auction = new AccountAuctionConfig(secondaryBidders: [GENERIC]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Set up openx bidder response with delay" + openXBidder.setResponseWithDilay(1) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXAndIXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder request" + def genericBidderRequests = bidder.getBidderRequests(bidRequest.id) + def openXBidderRequests = openXBidder.getBidderRequests(bidRequest.id) + assert genericBidderRequests.size() == 1 + assert openXBidderRequests.size() == 1 + + and: "PBS shouldn't contain errors, warnings and seat non bid" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + assert !bidResponse.ext.seatnonbid + + cleanup: "Reset mock" + openXBidder.reset() + bidder.reset() + } +} + +