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/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index 26e9ddc2057..ccbe970252f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -170,4 +170,16 @@ class BidRequest { ext.prebid.events = new Events() } } + + void enabledReturnAllBidStatus() { + if (!ext) { + ext = new BidRequestExt() + } + if (!ext.prebid) { + ext.prebid = new Prebid() + } + if (!ext.prebid.returnAllBidStatus) { + ext.prebid.returnAllBidStatus = true + } + } } 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/PbsServiceFactory.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy index e0911a2b1ca..2e499815e39 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy @@ -50,6 +50,9 @@ class PbsServiceFactory { static void removeContainer(Map config) { def container = containers.get(config) + if (container == null) { + throw new IllegalArgumentException("Unknown or invalid container config: " + config) + } container.stop() containers.remove(config) } 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..3fb31e61b11 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,8 @@ 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.MILLISECONDS +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 +49,13 @@ class Bidder extends NetworkScaffolding { : HttpResponse.notFoundResponse()} } + void setResponseWithDelay(Long dilayTimeoutMillisecond = 5000) { + mockServerClient.when(request().withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond {request -> request.withPath(endpoint) + ? response().withDelay(MILLISECONDS, dilayTimeoutMillisecond).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..e95b22b8bb0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy @@ -0,0 +1,286 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic +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 org.prebid.server.functional.util.PBSUtils +import spock.lang.Shared + +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +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.UNKNOWN +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_TIMED_OUT +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class SecondaryBidderSpec extends BaseSpec { + + private static final String OPENX_AUCTION_ENDPOINT = "/openx-auction" + private static final String GENERIC_ALIAS_AUCTION_ENDPOINT = "/generic-alias-auction" + private static final Map OPENX_CONFIG = [ + "adapters.${OPENX.value}.enabled" : "true", + "adapters.${OPENX.value}.endpoint": "$networkServiceContainer.rootUri$OPENX_AUCTION_ENDPOINT".toString()] + private static final Map GENERIC_ALIAS_CONFIG = [ + "adapters.${GENERIC.value}.aliases.${ALIAS}.enabled" : "true", + "adapters.${GENERIC.value}.aliases.${ALIAS}.endpoint": "$networkServiceContainer.rootUri$GENERIC_ALIAS_AUCTION_ENDPOINT".toString()] + private static final String WARNING_TIME_OUT_MESSAGE = "secondary bidder timed out, auction proceeded" + private static final Long RESPONSE_DELAY_MILLISECONDS = 5000 + private static final Bidder openXBidder = new Bidder(networkServiceContainer, OPENX_AUCTION_ENDPOINT) + private static final Bidder genericAliasBidder = new Bidder(networkServiceContainer, GENERIC_ALIAS_AUCTION_ENDPOINT) + + @Shared + PrebidServerService pbsServiceWithOpenXBidder = pbsServiceFactory.getService(OPENX_CONFIG + GENERIC_ALIAS_CONFIG) + + @Override + def cleanupSpec() { + pbsServiceFactory.removeContainer(OPENX_CONFIG + GENERIC_ALIAS_CONFIG) + } + + def cleanup() { + openXBidder.reset() + genericAliasBidder.reset() + } + + def "PBS shouldn't emit warning when secondary bidders config set to #secondaryBidder"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + enabledReturnAllBidStatus() + } + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + it.auction = new AccountAuctionConfig(secondaryBidders: [secondaryBidder]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should process bidder request" + assert bidder.getBidderRequests(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 + + where: + secondaryBidder << [null, UNKNOWN] + } + + def "PBS should treat all bidders as primary when all requested bidders in secondary bidders config"() { + given: "Default basic BidRequest with generic and openx bidder" + def bidRequest = getEnrichedBidRequest([GENERIC, OPENX]) + + 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) + + and: "Set up openx response" + openXBidder.setResponse() + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed generic request" + def genericBidderRequests = bidder.getBidderRequests(bidRequest.id) + assert genericBidderRequests.size() == 1 + + and: "PBs should processed openx request" + def openXBidderRequests = openXBidder.getBidderRequests(bidRequest.id) + 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 + } + + def "PBS shouldn't wait on non-prioritized bidder when primary bidder responds"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = getEnrichedBidRequest([GENERIC, 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.setResponseWithDelay(RESPONSE_DELAY_MILLISECONDS) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should processed bidder call" + assert bidder.getBidderRequests(bidRequest.id) + assert openXBidder.getBidderRequest(bidRequest.id) + + and: "PBs response shouldn't contain response body from openX bidder" + assert !bidResponse?.ext?.debug?.httpcalls[OPENX.value]?.responseBody + + and: "PBS shouldn't contain error for openX due to timeout" + assert !bidResponse.ext?.errors + + and: "PBs should respond with warning for openx" + assert bidResponse.ext?.warnings[ErrorType.OPENX].message == [WARNING_TIME_OUT_MESSAGE] + + and: "PBs should populate seatNonBid for openX bidder" + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == ERROR_TIMED_OUT + } + + def "PBS shouldn't treat alias bidder as secondary when root bidder code in secondary"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = getEnrichedBidRequest([OPENX, ALIAS]).tap { + it.ext.prebid.aliases = [(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.setResponseWithDelay(RESPONSE_DELAY_MILLISECONDS) + + and: "Set up openx alias bidder response" + genericAliasBidder.setResponse() + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) + + then: "PBS should process bidder request" + assert bidder.getBidderRequest(bidRequest.id) + assert genericAliasBidder.getBidderRequest(bidRequest.id) + assert openXBidder.getBidderRequest(bidRequest.id) + + and: "PBs response should contain openX alias and generic" + assert bidResponse.seatbid.seat.sort() == [ALIAS, GENERIC].sort() + + and: "PBs response should contain response body from generic and alias bidder" + def httpCalls = bidResponse?.ext?.debug?.httpcalls + assert httpCalls[GENERIC.value]?.responseBody + assert httpCalls[ALIAS.value]?.responseBody + + and: "PBS response shouldn't contain response body from openX bidder" + assert !httpCalls[OPENX.value]?.responseBody + + and: "PBS shouldn't contain error for openX due to timeout" + assert !bidResponse.ext?.errors + + and: "PBs should respond with warning for openx" + assert bidResponse.ext?.warnings[ErrorType.OPENX].message == [WARNING_TIME_OUT_MESSAGE] + + 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 == ERROR_TIMED_OUT + } + + def "PBS shouldn't wait on secondary bidder when alias bidder respond with delay"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = getEnrichedBidRequest([OPENX, ALIAS]).tap { + it.ext.prebid.aliases = [(ALIAS.value): OPENX] + } + + and: "Account in the DB" + def accountConfig = AccountConfig.defaultAccountConfig.tap { + it.auction = new AccountAuctionConfig(secondaryBidders: [ALIAS]) + } + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Set up openx bidder response with delay" + genericAliasBidder.setResponseWithDelay(RESPONSE_DELAY_MILLISECONDS) + + and: "Set up openx alias bidder response" + openXBidder.setResponse() + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should process bidder request" + assert bidder.getBidderRequest(bidRequest.id) + assert genericAliasBidder.getBidderRequest(bidRequest.id) + assert openXBidder.getBidderRequest(bidRequest.id) + + and: "PBs repose shouldn't contain response body from openX bidder" + assert !bidResponse?.ext?.debug?.httpcalls[ALIAS.value]?.responseBody + + and: "PBS should contain error for openX due to timeout" + assert !bidResponse.ext?.errors + + and: "PBs should respond with warning for openx alias" + assert bidResponse.ext?.warnings[ErrorType.ALIAS].message == [WARNING_TIME_OUT_MESSAGE] + + and: "PBs should populate seatNonBid" + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == ALIAS + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == ERROR_TIMED_OUT + } + + def "PBS should pass auction as usual when primary bidder responds after secondary"() { + given: "Default bid request with generic and openX bidders" + def bidRequest = getEnrichedBidRequest([GENERIC, OPENX]) + + 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" + def openXRandomDelay = bidRequest.tmax - PBSUtils.getRandomNumber(100, 500) + openXBidder.setResponseWithDelay(openXRandomDelay) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) + + then: "PBs should process bidder request" + assert bidder.getBidderRequest(bidRequest.id) + assert openXBidder.getBidderRequest(bidRequest.id) + + and: "PBs response should contain generic and openX bidders" + assert bidResponse.seatbid.seat.sort() == [GENERIC, OPENX].sort() + + and: "PBS shouldn't contain errors, warnings and seat non bid" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + assert !bidResponse.ext?.seatnonbid + } + + private static BidRequest getEnrichedBidRequest(List bidderNames) { + BidRequest.defaultBidRequest.tap { + if (bidderNames.contains(GENERIC)) { + it.imp[0]?.ext?.prebid?.bidder?.generic = new Generic() + } + if (bidderNames.contains(OPENX)) { + it.imp[0]?.ext?.prebid?.bidder?.openx = Openx.defaultOpenx + } + if (bidderNames.contains(ALIAS)) { + it.imp[0]?.ext?.prebid?.bidder?.alias = new Generic() + } + enabledReturnAllBidStatus() + } + } +}