From 6e2d40b4ef9309700151c937dba1e786aa6faee4 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Fri, 18 Jul 2025 15:16:24 +0300 Subject: [PATCH 1/3] Tests: Account config for limiting number of impressions --- .../model/config/AccountAuctionConfig.groovy | 4 + .../functional/tests/AuctionSpec.groovy | 157 +++++++++++++++++- 2 files changed, 158 insertions(+), 3 deletions(-) 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 bf49ce7c874..21a60bef192 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 @@ -35,6 +35,7 @@ class AccountAuctionConfig { @JsonProperty("bidadjustments") BidAdjustment bidAdjustments BidRounding bidRounding + Integer impressionLimit @JsonProperty("price_granularity") PriceGranularityType priceGranularitySnakeCase @@ -54,4 +55,7 @@ class AccountAuctionConfig { AccountPriceFloorsConfig priceFloorsSnakeCase @JsonProperty("bid_rounding") BidRounding bidRoundingSnakeCase + @JsonProperty("impression_limit") + Integer impressionLimitSnakeCase + } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy index a9bc17dfe1d..2c75300dea0 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -9,6 +9,7 @@ import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.DeviceExt +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.Renderer import org.prebid.server.functional.model.request.auction.RendererData @@ -47,15 +48,17 @@ class AuctionSpec extends BaseSpec { private static final Integer DEFAULT_TIMEOUT = getRandomTimeout() private static final Integer MIN_BID_ID_LENGTH = 17 private static final Integer DEFAULT_UUID_LENGTH = 36 - private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, - "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] private static final Map GENERIC_CONFIG = [ "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] - @Shared PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG) + private static final String IMPS_REQUESTED_METRIC = 'imps_requested' + private static final String IMPS_DROPPED_METRIC = 'imps_dropped' + private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, + "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] + def "PBS should return version in response header for auction request for #description"() { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequestRaw(bidRequest) @@ -721,4 +724,152 @@ class AuctionSpec extends BaseSpec { cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(pbsConfig) } + + def "PBS should drop extra impressions with warnings when number of impressions exceeds impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def impressionLimit = 1 + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "PBS should emit an warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == + ["Only first $impressionLimit impressions were kept due to the limit, " + + "all the subsequent impressions have been dropped for the auction" as String] + + and: "PBS shouldn't emit an error" + assert !response.ext?.errors + + and: "Metrics for imps should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_DROPPED_METRIC] == bidRequest.imp.size() - impressionLimit + assert metrics[IMPS_REQUESTED_METRIC] == impressionLimit + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == impressionLimit + + where: + accountAuctionConfig << [ + new AccountAuctionConfig(impressionLimit: impressionLimit), + new AccountAuctionConfig(impressionLimitSnakeCase: impressionLimit) + ] + } + + def "PBS shouldn't drop extra impressions when number of impressions equal to impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: bidRequest.imp.size())) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_DROPPED_METRIC] == 0 + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + } + + def "PBS shouldn't drop extra impressions when number of impressions less than or equal to impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def impressionLimit = bidRequest.imp.size() + 1 + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_DROPPED_METRIC] == 0 + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + } + + def "PBS shouldn't drop extra impressions when impression-limit set to #impressionLimit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_DROPPED_METRIC] == 0 + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + where: + impressionLimit << [null, 0] + } } From 91156e972661b27d0e6e7e906da3b4a1a7de3238 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Fri, 18 Jul 2025 18:57:24 +0300 Subject: [PATCH 2/3] Update tests --- .../functional/tests/AuctionSpec.groovy | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy index 2c75300dea0..8653de4258c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -56,6 +56,7 @@ class AuctionSpec extends BaseSpec { private static final String IMPS_REQUESTED_METRIC = 'imps_requested' private static final String IMPS_DROPPED_METRIC = 'imps_dropped' + private static final Integer IMP_LIMIT = 1 private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] @@ -732,7 +733,7 @@ class AuctionSpec extends BaseSpec { } and: "Account in the DB with impression limit config" - def impressionLimit = 1 + def accountConfig = new AccountConfig(auction: accountAuctionConfig) def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) accountDao.save(account) @@ -749,7 +750,7 @@ class AuctionSpec extends BaseSpec { and: "PBS should emit an warning" assert response.ext?.warnings[PREBID]*.code == [999] assert response.ext?.warnings[PREBID]*.message == - ["Only first $impressionLimit impressions were kept due to the limit, " + + ["Only first $IMP_LIMIT impressions were kept due to the limit, " + "all the subsequent impressions have been dropped for the auction" as String] and: "PBS shouldn't emit an error" @@ -757,16 +758,16 @@ class AuctionSpec extends BaseSpec { and: "Metrics for imps should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics[IMPS_DROPPED_METRIC] == bidRequest.imp.size() - impressionLimit - assert metrics[IMPS_REQUESTED_METRIC] == impressionLimit + assert metrics[IMPS_DROPPED_METRIC] == bidRequest.imp.size() - IMP_LIMIT + assert metrics[IMPS_REQUESTED_METRIC] == IMP_LIMIT and: "Response should contain seat bid" - assert response.seatbid[0].bid.size() == impressionLimit + assert response.seatbid[0].bid.size() == IMP_LIMIT where: accountAuctionConfig << [ - new AccountAuctionConfig(impressionLimit: impressionLimit), - new AccountAuctionConfig(impressionLimitSnakeCase: impressionLimit) + new AccountAuctionConfig(impressionLimit: IMP_LIMIT), + new AccountAuctionConfig(impressionLimitSnakeCase: IMP_LIMIT) ] } @@ -796,8 +797,8 @@ class AuctionSpec extends BaseSpec { and: "Metrics for imps requested should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics[IMPS_DROPPED_METRIC] == 0 assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == bidRequest.imp.size() @@ -830,8 +831,8 @@ class AuctionSpec extends BaseSpec { and: "Metrics for imps requested should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics[IMPS_DROPPED_METRIC] == 0 assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == bidRequest.imp.size() @@ -863,8 +864,8 @@ class AuctionSpec extends BaseSpec { and: "Metrics for imps requested should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics[IMPS_DROPPED_METRIC] == 0 assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == bidRequest.imp.size() From 6f4ee17cc82290c0ce5be55f36b4f20d1f66d271 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Fri, 25 Jul 2025 13:02:09 +0300 Subject: [PATCH 3/3] Update after review --- .../server/functional/tests/AuctionSpec.groovy | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy index 8653de4258c..1506e2e0a4d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -733,7 +733,6 @@ class AuctionSpec extends BaseSpec { } and: "Account in the DB with impression limit config" - def accountConfig = new AccountConfig(auction: accountAuctionConfig) def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) accountDao.save(account) @@ -764,6 +763,9 @@ class AuctionSpec extends BaseSpec { and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == IMP_LIMIT + and: "Bidder request should contain imps according to limit" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == IMP_LIMIT + where: accountAuctionConfig << [ new AccountAuctionConfig(impressionLimit: IMP_LIMIT), @@ -802,6 +804,9 @@ class AuctionSpec extends BaseSpec { and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() } def "PBS shouldn't drop extra impressions when number of impressions less than or equal to impression-limit"() { @@ -836,6 +841,9 @@ class AuctionSpec extends BaseSpec { and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() } def "PBS shouldn't drop extra impressions when impression-limit set to #impressionLimit"() { @@ -870,7 +878,10 @@ class AuctionSpec extends BaseSpec { and: "Response should contain seat bid" assert response.seatbid[0].bid.size() == bidRequest.imp.size() + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + where: - impressionLimit << [null, 0] + impressionLimit << [null, PBSUtils.randomNegativeNumber, 0] } }