From ea5c43952dc3ec8ccf133613acff212b412a33b7 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Wed, 16 Sep 2026 19:25:49 +0300 Subject: [PATCH 1/4] Tests: fix flaky functional tests --- .../functional/testcontainers/PbsConfig.groovy | 2 ++ .../scaffolding/VendorList.groovy | 13 +++++++++++++ .../functional/tests/SecondaryBidderSpec.groovy | 1 + .../pbruleengine/PbRuleEnginePrivacySpec.groovy | 17 +++++++++++++---- .../pricefloors/PriceFloorsSignalingSpec.groovy | 2 +- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 1163c070e1e..c873c1a03f3 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -36,6 +36,8 @@ LIMIT 1 "metrics.prefix" : "prebid", "status-response" : "ok", "gdpr.default-value" : "0", + "gdpr.vendorlist.v2.http-endpoint-template" : "$networkServiceContainer.rootUri/v2/vendor-list.json".toString(), + "gdpr.vendorlist.v3.http-endpoint-template" : "$networkServiceContainer.rootUri/v3/vendor-list.json".toString(), "settings.database.account-query" : DB_ACCOUNT_QUERY, "settings.database.stored-requests-query" : "SELECT accountId, reqId, requestData, 'request' as dataType FROM stored_requests WHERE reqId IN (%REQUEST_ID_LIST%) UNION ALL SELECT accountId, impId, impData, 'imp' as dataType FROM stored_imps WHERE impId IN (%IMP_ID_LIST%)", "settings.database.amp-stored-requests-query": "SELECT accountId, reqId, requestData, 'request' as dataType FROM stored_requests WHERE reqId IN (%REQUEST_ID_LIST%)", diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy index ec561b073b7..7e1d1e3db07 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy @@ -11,6 +11,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR import static org.apache.http.HttpStatus.SC_OK import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V2 import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 @@ -83,4 +84,16 @@ class VendorList extends NetworkScaffolding { ) } } + + void setErrorResponse(TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, + Integer status = SC_INTERNAL_SERVER_ERROR) { + + def preparedEndpoint = endpoint.replace("{TCF_POLICY}", tcfPolicyVersion.vendorListVersion.toString()) + + wireMockClient.register(any(urlMatching(preparedEndpoint)) + .atPriority(Integer.MAX_VALUE) + .willReturn(aResponse() + .withStatus(status)) + ) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy index b38b1c18794..8370dfbdd6f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy @@ -461,6 +461,7 @@ class SecondaryBidderSpec extends BaseSpec { if (bidderNames.contains(ALIAS)) { it.imp[0]?.ext?.prebid?.bidder?.alias = new Generic() } + it.tmax = 5_000 enabledReturnAllBidStatus() } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/PbRuleEnginePrivacySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/PbRuleEnginePrivacySpec.groovy index 25804dc614e..64106286821 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/PbRuleEnginePrivacySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/PbRuleEnginePrivacySpec.groovy @@ -31,6 +31,7 @@ import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_S import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_IN import static org.prebid.server.functional.model.config.RuleEngineFunction.TCF_IN_SCOPE import static org.prebid.server.functional.model.config.RuleEngineFunction.USER_FPD_AVAILABLE +import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE @@ -39,6 +40,14 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BAS class PbRuleEnginePrivacySpec extends PbRuleEngineBaseSpec { + def setupSpec() { + vendorList.setErrorResponse() + } + + def cleanupSpec() { + vendorList.reset() + } + def "PBS should exclude bidder when eidAvailable match with condition"() { given: "Bid request with multiply bidders" def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { @@ -538,7 +547,7 @@ class PbRuleEnginePrivacySpec extends PbRuleEngineBaseSpec { def "PBS should exclude bidder when gppSidAvailable match with condition"() { given: "Default bid request with multiply bidder" def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { - regs = new Regs(gppSid: [PBSUtils.getRandomEnum(GppSectionId).getIntValue()]) + regs = new Regs(gppSid: [PBSUtils.getRandomEnum(GppSectionId, [TCF_EU_V2]).getIntValue()]) } and: "Account with rule engine config" @@ -641,7 +650,7 @@ class PbRuleEnginePrivacySpec extends PbRuleEngineBaseSpec { and: "Default bid request with multiply bidders" def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { - regs = new Regs(gdpr: 0, gppSid: [PBSUtils.getRandomEnum(GppSectionId, [GppSectionId.TCF_EU_V2]).getIntValue()]) + regs = new Regs(gdpr: 0, gppSid: [PBSUtils.getRandomEnum(GppSectionId, [TCF_EU_V2]).getIntValue()]) } and: "Account with rule engine config" @@ -745,7 +754,7 @@ class PbRuleEnginePrivacySpec extends PbRuleEngineBaseSpec { assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE where: - gppSectionId << GppSectionId.values() - GppSectionId.TCF_EU_V2 + gppSectionId << GppSectionId.values() - TCF_EU_V2 } def "PBS shouldn't exclude bidder when gppSidIn not match with condition"() { @@ -789,7 +798,7 @@ class PbRuleEnginePrivacySpec extends PbRuleEngineBaseSpec { assert !getAnalyticResults(bidResponse) where: - gppSectionId << GppSectionId.values() - GppSectionId.TCF_EU_V2 + gppSectionId << GppSectionId.values() - TCF_EU_V2 } def "PBS should exclude bidder when tcfInScope match with condition"() { 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 e9b819d23fa..74fec42e556 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 @@ -1148,6 +1148,6 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { } private static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { - floorValue.divide(bidAdjustment, FLOOR_VALUE_PRECISION, RoundingMode.HALF_UP) + floorValue.divide(bidAdjustment, FLOOR_VALUE_PRECISION, RoundingMode.HALF_EVEN) } } From e609c2223be7b840a12679ba2d7607d802bc8643 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 17 Sep 2026 11:15:18 +0300 Subject: [PATCH 2/4] Fix global metric assertion due to shared histogram state --- .../org/prebid/server/functional/tests/CacheSpec.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index c5974022061..372d5045817 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -72,7 +72,7 @@ class CacheSpec extends BaseSpec { and: "prebid_cache.creative_size.json metric should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() assert metrics[REQUEST_OK_METRIC] == initialValue + 1 - assert metrics[JSON_CREATIVE_SIZE_GLOBAL_METRIC] == creativeSize + assert metrics[JSON_CREATIVE_SIZE_GLOBAL_METRIC] and: "account..prebid_cache.creative_size.json should be update" assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 @@ -108,7 +108,7 @@ class CacheSpec extends BaseSpec { and: "prebid_cache.creative_size.json metric should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() assert metrics[REQUEST_OK_METRIC] == initialValue + 1 - assert metrics[XML_CREATIVE_SIZE_GLOBAL_METRIC] == creativeSize + assert metrics[XML_CREATIVE_SIZE_GLOBAL_METRIC] and: "account..prebid_cache.creative_size.json should be update" assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 From f5bd149dbbcd22fed8935171616e573bde430304 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Mon, 21 Sep 2026 21:01:23 +0300 Subject: [PATCH 3/4] Update secondary bidder detection --- .../tests/SecondaryBidderSpec.groovy | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy index 8370dfbdd6f..706e5a9d648 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SecondaryBidderSpec.groovy @@ -129,8 +129,11 @@ class SecondaryBidderSpec extends BaseSpec { when: "PBS processes auction request" def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) - then: "PBs should processed bidder call" + then: "PBS should process request for primary bidder" assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should process request for secondary bidder" + PBSUtils.waitUntil({ !openXBidder.getBidderRequests(bidRequest.id).isEmpty() }) assert openXBidder.getBidderRequest(bidRequest.id) and: "PBs response shouldn't contain response body from openX bidder" @@ -340,8 +343,11 @@ class SecondaryBidderSpec extends BaseSpec { when: "PBS processes auction request" def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) - then: "PBs should processed bidder call" + then: "PBS should process request for primary bidder" assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should process request for secondary bidder" + PBSUtils.waitUntil({ !openXBidder.getBidderRequests(bidRequest.id).isEmpty() }) assert openXBidder.getBidderRequest(bidRequest.id) and: "PBs response shouldn't contain response body from openX bidder" @@ -376,9 +382,12 @@ class SecondaryBidderSpec extends BaseSpec { when: "PBS processes auction request" def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) - then: "PBS should process bidder request" - assert bidder.getBidderRequest(bidRequest.id) + then: "PBS should process request for primary and secondary bidder" + assert bidder.getBidderRequests(bidRequest.id) assert genericAliasBidder.getBidderRequest(bidRequest.id) + + and: "PBS should process request for secondary bidder" + PBSUtils.waitUntil({ !openXBidder.getBidderRequests(bidRequest.id).isEmpty() }) assert openXBidder.getBidderRequest(bidRequest.id) and: "PBs response should contain openX alias and generic" @@ -424,8 +433,11 @@ class SecondaryBidderSpec extends BaseSpec { when: "PBS processes auction request" def bidResponse = pbsServiceWithOpenXBidder.sendAuctionRequest(bidRequest) - then: "PBs should processed bidder call" + then: "PBS should process request for primary bidder" assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should process request for secondary bidder" + PBSUtils.waitUntil({ !openXBidder.getBidderRequests(bidRequest.id).isEmpty() }) assert openXBidder.getBidderRequest(bidRequest.id) and: "PBs response shouldn't contain response body from openX bidder" From 7b0b26bffe257b1d8f50e6493d1a4c78684e1e6e Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 24 Sep 2026 14:08:28 +0300 Subject: [PATCH 4/4] Fix timeout log flakiness --- .../service/PrebidServerService.groovy | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index c7994152f35..d8b1a05ff31 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -59,6 +59,7 @@ import static org.prebid.server.functional.model.config.Endpoint.SETUID import static org.prebid.server.functional.model.config.Endpoint.STATUS import static org.prebid.server.functional.model.config.Endpoint.VTRACK import static org.prebid.server.functional.testcontainers.Dependencies.influxdbContainer +import static org.prebid.server.functional.util.SystemProperties.DEFAULT_TIMEOUT class PrebidServerService implements ObjectMapperWrapper { @@ -410,33 +411,17 @@ class PrebidServerService implements ObjectMapperWrapper { } } - List getLogsByTime(Instant testStart, Instant testEnd = Instant.now()) { - if (testEnd.isBefore(testStart)) { - throw new IllegalArgumentException("The end time of the test is less than the start time") - } - def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") - .withZone(ZoneId.from(UTC)) - def logs = Arrays.asList(pbsContainer.logs.split("\n")) - def filteredLogs = [] - - def deltaTime = Duration.between(testStart, testEnd).plusSeconds(1).seconds - - for (int i = 0; i <= deltaTime; i++) { - def time = testStart.plusSeconds(i) - def element = logs.find { it.contains(formatter.format(time)) } - if (element) { - filteredLogs.addAll(logs.subList(logs.indexOf(element), logs.size())) - break - } - } - filteredLogs - } - String getLogsByValue(String value) { if (!value) { throw new IllegalArgumentException("Value is null or empty") } - getPbsLogsByValue(value) + def matches = getPbsLogsByValue(value) + if (matches.size() > 1) { + throw new IllegalStateException( + "Expected exactly one log containing '$value', but found ${matches.size()}" + ) + } + matches ? matches.first() : null } Boolean isContainLogsByValue(String value) { @@ -448,15 +433,42 @@ class PrebidServerService implements ObjectMapperWrapper { } } - Boolean isMetricFilled(String metricName) { - try { - PBSUtils.waitUntil({ this.sendCollectedMetricsRequest()[metricName] != 0 }) - true - } catch (IllegalStateException ignored) { - false + private List getPbsLogsByValue(String value) { + getPbsLogs().findAll { it.contains(value) } + } + + private List getPbsLogs() { + pbsContainer.logs?.readLines() ?: [] + } + + List getLogsByTime(Instant testStart, long timeoutMs = DEFAULT_TIMEOUT) { + if (!testStart) { + throw new IllegalArgumentException("Test start time is null") } + def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").withZone(UTC) + List result = [] + + PBSUtils.waitUntil({ + result = findLogsByTime(testStart, getPbsLogs(), formatter) + !result.isEmpty() + }, timeoutMs) + result } + private static List findLogsByTime(Instant testStart, List logs, DateTimeFormatter formatter) { + def deltaTime = Duration.between(testStart, Instant.now()).plusSeconds(1).seconds + for (long i = 0; i <= deltaTime; i++) { + def timestamp = formatter.format(testStart.plusSeconds(i)) + def index = logs.findIndexOf { it.contains(timestamp) } + + if (index >= 0) { + return logs[index..-1] + } + } + [] + } + + Boolean isContainMetricByValue(String value) { try { PBSUtils.waitUntil({ sendInfluxMetricsRequest()[value] != null }) @@ -466,10 +478,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - private String getPbsLogsByValue(String value) { - pbsContainer.logs.split("\n").find { it.contains(value) } - } - T getValueFromContainer(String path, Class clazz) { pbsContainer.copyFileFromContainer(path, { inputStream -> return decode(inputStream, clazz)