From 9a17096678d105b9fb93c4f01b74c564cb15b2bc Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Wed, 22 Apr 2026 20:00:14 +0300 Subject: [PATCH 1/4] Tests: Handle GVL deletions --- .../vendorlist/VendorListResponse.groovy | 5 +- .../tests/privacy/GdprAuctionSpec.groovy | 178 +++++++++++++++++- .../tests/privacy/PrivacyBaseSpec.groovy | 12 ++ .../functional/util/privacy/TcfConsent.groovy | 6 +- 4 files changed, 195 insertions(+), 6 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy index d44755978e4..9e54f4ce7e6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy @@ -34,6 +34,7 @@ class VendorListResponse { List features List specialFeatures String policyUrl + ZonedDateTime deletedDate Overflow overflow String cookieMaxAgeSeconds Boolean usesCookies @@ -41,9 +42,9 @@ class VendorListResponse { Boolean usesNonCookieAccess Boolean deviceStorageDisclosureUrl - static Vendor getDefaultVendor(int id) { + static Vendor getDefaultVendor(int vendorId) { new Vendor().tap { - it.id = id + it.id = vendorId it.name = PBSUtils.randomString it.purposes = [1, 3, 4, 5] it.legIntPurposes = [2, 7, 10] diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 299d911a398..fc4904516b5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -7,27 +7,32 @@ import org.prebid.server.functional.model.config.AccountMetricsConfig import org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.config.PurposeEnforcement +import org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse import org.prebid.server.functional.model.pricefloors.Country +import org.prebid.server.functional.model.privacy.EnforcementRequirement import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.Regs import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.NoBidResponse import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.TcfConsent +import org.prebid.server.functional.util.privacy.TcfUtils import org.prebid.server.functional.util.privacy.VendorListConsent import spock.lang.PendingFeature import java.time.Instant +import java.time.ZonedDateTime import static org.prebid.server.functional.model.ChannelType.PBJS import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.GENERIC - import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED import static org.prebid.server.functional.model.config.Purpose.P1 import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 +import static org.prebid.server.functional.model.config.PurposeEnforcement.FULL import static org.prebid.server.functional.model.config.PurposeEnforcement.NO import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA @@ -48,6 +53,8 @@ import static org.prebid.server.functional.model.response.auction.BidRejectionRe import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS +import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.REQUIRE_CONSENT +import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.UNDEFINED import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V4 import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V5 @@ -1099,4 +1106,173 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 assert metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 } + + def "PBS should treated GVL record as invalid when it's marked as deleted for GVL file"() { + given: "Valid tcf full enforcement requirments" + def vendorListVersion = PBSUtils.getRandomNumber(1, 4095) + def requirement = new EnforcementRequirement(purpose: P2, + restrictionType: [REQUIRE_CONSENT, UNDEFINED], + vendorIdGvl: GENERIC_VENDOR_ID, + enforcePurpose: FULL, + vendorConsentBitField: GENERIC_VENDOR_ID, + enforceVendor: true, + vendorListVersion: vendorListVersion) + + and: "Tcf consent setup" + def tcfConsent = TcfUtils.getConsentString(requirement) + + and: "Bid request with tcf consent string" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Account with tcf config" + def accountGdprConfig = new AccountGdprConfig(purposes: TcfUtils.getPurposeConfigsForPersonalizedAds(requirement)) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Set vendor list response with passed delete date for bidder" + def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = ZonedDateTime.now().minusSeconds(1) + } + downloadAndWarmupGvtList(vendorListVersion, [(GENERIC_VENDOR_ID): vendorList]) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain any seatBids, errors and warnings" + assert verifyAll(response) { + !it.seatbid.size() + !it.ext.warnings + !it.ext.errors + } + + and: "Response should include nbr" + assert response.noBidResponse == NoBidResponse.UNKNOWN_ERROR + + and: "PBS should cansel request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Disallowed metrics should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + } + + def "PBS should treated GVL record as valid when it's don't marked as deleted for GVL file"() { + given: "Valid tcf full enforcement requirments" + def vendorListVersion = PBSUtils.getRandomNumber(1, 4095) + def requirement = new EnforcementRequirement(purpose: P2, + restrictionType: [REQUIRE_CONSENT, UNDEFINED], + vendorIdGvl: GENERIC_VENDOR_ID, + enforcePurpose: FULL, + vendorConsentBitField: GENERIC_VENDOR_ID, + enforceVendor: true, + vendorListVersion: vendorListVersion) + + and: "Tcf consent setup" + def tcfConsent = TcfUtils.getConsentString(requirement) + + and: "Bid request with tcf consent string" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Account with tcf config" + def accountGdprConfig = new AccountGdprConfig(purposes: TcfUtils.getPurposeConfigsForPersonalizedAds(requirement)) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Set vendor list response with passed delete date for bidder" + def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = ZonedDateTime.now().minusSeconds(1) + } + downloadAndWarmupGvtList(vendorListVersion, [(GENERIC_VENDOR_ID): vendorList]) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + "Response shouldn't contain any nrb, errors and warnings" + assert verifyAll(response) { + !it.noBidResponse + !it.ext.warnings + !it.ext.errors + } + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Disallowed metrics shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 0 + + where: + gvlDeletedDate << [null, ZonedDateTime.now().plusYears(1)] + } + + def "PBS should treated GVL record as invalid when it's marked as deleted for newer GVL file"() { + given: "Default PBS service with privacy" + def privacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "Set latest vendor list response with passed delete date for bidder" + def vendorListVersion = PBSUtils.getRandomNumber(2, 4095) + def latestListWithExpiredVendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = ZonedDateTime.now().minusSeconds(1) + } + downloadAndWarmupGvtList(vendorListVersion, [(GENERIC_VENDOR_ID): latestListWithExpiredVendor]) + + and: "Valid tcf full enforcement requirments" + def requirement = new EnforcementRequirement(purpose: P2, + restrictionType: [REQUIRE_CONSENT, UNDEFINED], + vendorIdGvl: GENERIC_VENDOR_ID, + enforcePurpose: FULL, + vendorConsentBitField: GENERIC_VENDOR_ID, + enforceVendor: true, + vendorListVersion: vendorListVersion - 1) + + and: "Tcf consent setup" + def tcfConsent = TcfUtils.getConsentString(requirement) + + and: "Bid request with tcf consent string" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Account with tcf config" + def accountGdprConfig = new AccountGdprConfig(purposes: TcfUtils.getPurposeConfigsForPersonalizedAds(requirement)) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Set older vendor list response with unexpired bidder" + def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID) + downloadAndWarmupGvtList(vendorListVersion - 1, [(GENERIC_VENDOR_ID): vendorList]) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain any seatBids, errors and warnings" + assert verifyAll(response) { + !it.seatbid.size() + !it.ext.warnings + !it.ext.errors + } + + and: "Response should include nbr" + assert response.noBidResponse == NoBidResponse.UNKNOWN_ERROR + + and: "PBS should cansel request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Disallowed metrics should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index 7d8ed79de7d..0148128de4a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -674,4 +674,16 @@ abstract class PrivacyBaseSpec extends BaseSpec { def availablePurposes = Purpose.values().toList() - excludeFromRandom availablePurposes.shuffled().first() } + + protected void downloadAndWarmupGvtList(Integer vendorListVersion, + Map vendors = [(GENERIC_VENDOR_ID): VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID)], + TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, + PrebidServerService pbsService = privacyPbsService) { + + def simpleTcfString = new TcfConsent.Builder().setVendorListVersion(vendorListVersion).build() + def bidRequest = getGdprBidRequest(simpleTcfString) + vendorListResponse.reset() + vendorListResponse.setResponse(tcfPolicyVersion, null, vendors) + pbsService.sendAuctionRequest(bidRequest) + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/privacy/TcfConsent.groovy b/src/test/groovy/org/prebid/server/functional/util/privacy/TcfConsent.groovy index cdb6fce665b..5c9ce8c5286 100644 --- a/src/test/groovy/org/prebid/server/functional/util/privacy/TcfConsent.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/privacy/TcfConsent.groovy @@ -10,9 +10,9 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVers class TcfConsent implements ConsentString { - public static final Integer RUBICON_VENDOR_ID = PBSUtils.getRandomNumber(0, 65534) - public static final Integer GENERIC_VENDOR_ID = PBSUtils.getRandomNumber(0, 65534) - public static final Integer VENDOR_LIST_VERSION = PBSUtils.getRandomNumber(0, 4095) + public static final Integer RUBICON_VENDOR_ID = PBSUtils.getRandomNumber(1, 65534) + public static final Integer GENERIC_VENDOR_ID = PBSUtils.getRandomNumber(1, 65534) + public static final Integer VENDOR_LIST_VERSION = PBSUtils.getRandomNumber(1, 4095) private final TCStringEncoder.Builder tcStringEncoder From e71f56099142aba8fb20ea5383deff54b4e76124 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Mon, 8 Jun 2026 15:16:02 +0300 Subject: [PATCH 2/4] update tests due new requirments --- .../scaffolding/VendorList.groovy | 39 ++-- .../tests/privacy/PrivacyBaseSpec.groovy | 30 +-- .../tests/privacy/gdpr/GdprAuctionSpec.groovy | 220 +++++++++++++----- 3 files changed, 196 insertions(+), 93 deletions(-) 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 343a118f53a..9e079f0bc02 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 @@ -4,7 +4,7 @@ import org.mockserver.matchers.TimeToLive import org.mockserver.matchers.Times import org.mockserver.model.Delay import org.mockserver.model.HttpRequest -import org.mockserver.model.HttpResponse +import org.prebid.server.functional.util.privacy.TcfConsent import org.testcontainers.containers.MockServerContainer import static org.mockserver.model.HttpRequest.request @@ -20,10 +20,13 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVers class VendorList extends NetworkScaffolding { - private static final String VENDOR_LIST_ENDPOINT = "/v{TCF_POLICY}/vendor-list.json" + private static final String ENDPOINT_TEMPLATE = "/v{TCF_POLICY}/{FILE_NAME}" - VendorList(MockServerContainer mockServerContainer) { - super(mockServerContainer, VENDOR_LIST_ENDPOINT) + private final String fileName + + VendorList(MockServerContainer mockServerContainer, String fileName = "vendor-list.json") { + super(mockServerContainer, ENDPOINT_TEMPLATE.replace("{FILE_NAME}", fileName)) + this.fileName = fileName } @Override @@ -33,29 +36,39 @@ class VendorList extends NetworkScaffolding { @Override protected HttpRequest getRequest() { - request().withPath(VENDOR_LIST_ENDPOINT) + request().withPath(endpoint) } @Override void reset() { - TcfPolicyVersion.values().each { version -> super.reset("/v${version.vendorListVersion}/vendor-list.json") } + TcfPolicyVersion.values().each { + super.reset("/v${it.vendorListVersion}/${fileName}") + } } void setResponse(TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, Delay delay = null, - Map vendors = [(GENERIC_VENDOR_ID): Vendor.getDefaultVendor(GENERIC_VENDOR_ID)]) { + Map vendors = [(GENERIC_VENDOR_ID): Vendor.getDefaultVendor(GENERIC_VENDOR_ID)], + vendorListVersion = TcfConsent.VENDOR_LIST_VERSION) { + def prepareEndpoint = endpoint.replace("{TCF_POLICY}", tcfPolicyVersion.vendorListVersion.toString()) def prepareEncodeResponseBody = encode(defaultVendorListResponse.tap { it.tcfPolicyVersion = tcfPolicyVersion.vendorListVersion it.vendors = vendors + it.vendorListVersion = vendorListVersion it.gvlSpecificationVersion = tcfPolicyVersion >= TcfPolicyVersion.TCF_POLICY_V4 ? V3 : V2 }) - mockServerClient.when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) - .respond { request -> - request.withPath(endpoint) - ? response().withStatusCode(OK_200.code()).withDelay(delay).withBody(prepareEncodeResponseBody) - : HttpResponse.notFoundResponse() - } + def mockResponse = response() + .withStatusCode(OK_200.code()) + .withBody(prepareEncodeResponseBody) + + if (delay != null) { + mockResponse.withDelay(delay) + } + + mockServerClient + .when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond(mockResponse) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index d3e42decb24..a0557a55202 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -8,10 +8,8 @@ import org.prebid.server.functional.model.config.AccountDsaConfig import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.config.AccountGppConfig import org.prebid.server.functional.model.config.AccountPrivacyConfig -import org.prebid.server.functional.model.config.Purpose import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse -import org.prebid.server.functional.model.privacy.EnforcementRequirement import org.prebid.server.functional.model.privacy.gpp.GppDataActivity import org.prebid.server.functional.model.privacy.gpp.UsCaliforniaV1ChildSensitiveData import org.prebid.server.functional.model.privacy.gpp.UsCaliforniaV1SensitiveData @@ -55,12 +53,9 @@ import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent 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.config.PurposeEnforcement.BASIC -import static org.prebid.server.functional.model.config.PurposeEnforcement.FULL -import static org.prebid.server.functional.model.config.PurposeEnforcement.NO import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse -import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA +import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 import static org.prebid.server.functional.model.request.GppSectionId.US_CT_V1 @@ -74,10 +69,6 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.REDIRECT import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID -import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS -import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.REQUIRE_CONSENT -import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.REQUIRE_LEGITIMATE_INTEREST -import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.UNDEFINED import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 import static org.prebid.server.functional.util.privacy.model.State.ALABAMA @@ -90,7 +81,7 @@ abstract class PrivacyBaseSpec extends BaseSpec { "adapters.${GENERIC.value}.ortb-version" : "2.6"] private static final Map OPENX_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value, - "adapters.${OPENX}.ortb-version" : "2.6", + "adapters.${OPENX.value}.ortb-version" : "2.6", "adapters.${OPENX.value}.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), "adapters.${OPENX.value}.enabled" : 'true'] protected static final Map GDPR_VENDOR_LIST_CONFIG = ["gdpr.vendorlist.v2.http-endpoint-template": "$networkServiceContainer.rootUri/v2/vendor-list.json".toString(), @@ -363,21 +354,4 @@ abstract class PrivacyBaseSpec extends BaseSpec { it.vendors = [(GENERIC_VENDOR_ID): vendor] }) } - - private static Purpose getRandomPurposeWithExclusion(Purpose excludeFromRandom) { - def availablePurposes = Purpose.values().toList() - excludeFromRandom - availablePurposes.shuffled().first() - } - - protected void downloadAndWarmupGvtList(Integer vendorListVersion, - Map vendors = [(GENERIC_VENDOR_ID): VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID)], - TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, - PrebidServerService pbsService = privacyPbsService) { - - def simpleTcfString = new TcfConsent.Builder().setVendorListVersion(vendorListVersion).build() - def bidRequest = getGdprBidRequest(simpleTcfString) - vendorListResponse.reset() - vendorListResponse.setResponse(tcfPolicyVersion, null, vendors) - pbsService.sendAuctionRequest(bidRequest) - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy index e8586053ecf..68942f587ee 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy @@ -14,8 +14,10 @@ import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.Regs import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ErrorType -import org.prebid.server.functional.tests.privacy.PrivacyBaseSpec import org.prebid.server.functional.model.response.auction.NoBidResponse +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.VendorList +import org.prebid.server.functional.tests.privacy.PrivacyBaseSpec import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.TcfConsent @@ -51,6 +53,7 @@ import static org.prebid.server.functional.model.request.auction.PublicCountryIp import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_PRIVACY +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS @@ -62,6 +65,21 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVers class GdprAuctionSpec extends PrivacyBaseSpec { + private static final String LIVE_GVL_FILE_NAME = "live-gvl-vendor-list.json" + private static final VENDOR_LIST_VERSION = PBSUtils.getRandomNumber(2, 4094) + private static final Map REFRESH_LIVE_VENDOR_LIST_CONFIG = GENERAL_PRIVACY_CONFIG + + ["gdpr.vendorlist.live-gvl-url": "$networkServiceContainer.rootUri/v3/$LIVE_GVL_FILE_NAME".toString()] + private static final VendorList liveVendorListResponse = new VendorList(networkServiceContainer, LIVE_GVL_FILE_NAME) + + def setup() { + vendorListResponse.setResponse() + liveVendorListResponse.reset() + } + + def cleanup() { + vendorListResponse.reset() + } + @PendingFeature def "PBS should add debug log for auction request when valid gdpr was passed"() { given: "Default gdpr BidRequest" @@ -1124,16 +1142,26 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 } - def "PBS should treated GVL record as invalid when it's marked as deleted for GVL file"() { - given: "Valid tcf full enforcement requirments" - def vendorListVersion = PBSUtils.getRandomNumber(1, 4095) + def "PBS should treated GVL record as valid when it's don't marked as deleted for GVL file"() { + given: "Vendor list response with not passed delete date for bidder" + def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = gvlDeletedDate + } + downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): vendorList]) + + and: "PBS with proper warmed GVL config" + def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) + warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION) + + and: "Valid tcf full enforcement requirments" def requirement = new EnforcementRequirement(purpose: P2, restrictionType: [REQUIRE_CONSENT, UNDEFINED], vendorIdGvl: GENERIC_VENDOR_ID, enforcePurpose: FULL, vendorConsentBitField: GENERIC_VENDOR_ID, enforceVendor: true, - vendorListVersion: vendorListVersion) + disclosedVendorsId: [GENERIC_VENDOR_ID], + vendorListVersion: VENDOR_LIST_VERSION) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1146,17 +1174,73 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) accountDao.save(account) - and: "Set vendor list response with passed delete date for bidder" + and: "Flush metric" + flushMetrics(pbsWithLiveGvlSetup) + + when: "PBS processes auction request" + def response = pbsWithLiveGvlSetup.sendAuctionRequest(bidRequest) + + then: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + "Response shouldn't contain any nrb, errors and warnings" + assert verifyAll(response) { + !it.noBidResponse + !it.ext.warnings + !it.ext.errors + } + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Disallowed metrics shouldn't be updated" + def metrics = pbsWithLiveGvlSetup.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 0 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(REFRESH_LIVE_VENDOR_LIST_CONFIG) + + where: + gvlDeletedDate << [null, ZonedDateTime.now().plusYears(1)] + } + + def "PBS should treated GVL record as invalid when it's marked as deleted for GVL file"() { + given: "Vendor list response with passed delete date for bidder" def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().minusSeconds(1) } - downloadAndWarmupGvtList(vendorListVersion, [(GENERIC_VENDOR_ID): vendorList]) + downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): vendorList]) + + and: "PBS with proper warmed GVL config" + def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) + warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION) + + and: "Valid tcf full enforcement requirments" + def requirement = new EnforcementRequirement(purpose: P2, + restrictionType: [REQUIRE_CONSENT, UNDEFINED], + vendorIdGvl: GENERIC_VENDOR_ID, + enforcePurpose: FULL, + vendorConsentBitField: GENERIC_VENDOR_ID, + enforceVendor: true, + disclosedVendorsId: [GENERIC_VENDOR_ID], + vendorListVersion: VENDOR_LIST_VERSION) + + and: "Tcf consent setup" + def tcfConsent = TcfUtils.getConsentString(requirement) + + and: "Bid request with tcf consent string" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Account with tcf config" + def accountGdprConfig = new AccountGdprConfig(purposes: TcfUtils.getPurposeConfigsForPersonalizedAds(requirement)) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) and: "Flush metric" - flushMetrics(privacyPbsService) + flushMetrics(pbsWithLiveGvlSetup) when: "PBS processes auction request" - def response = privacyPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithLiveGvlSetup.sendAuctionRequest(bidRequest) then: "Response shouldn't contain any seatBids, errors and warnings" assert verifyAll(response) { @@ -1172,20 +1256,33 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert !bidder.getBidderRequests(bidRequest.id) and: "Disallowed metrics should be updated" - def metrics = privacyPbsService.sendCollectedMetricsRequest() + def metrics = pbsWithLiveGvlSetup.sendCollectedMetricsRequest() assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(REFRESH_LIVE_VENDOR_LIST_CONFIG) } - def "PBS should treated GVL record as valid when it's don't marked as deleted for GVL file"() { - given: "Valid tcf full enforcement requirments" - def vendorListVersion = PBSUtils.getRandomNumber(1, 4095) + def "PBS should treated GVL record as invalid when it's marked as deleted for newer GVL file"() { + given: "Newer Vendor list response with passed delete date for bidder" + def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = ZonedDateTime.now().minusSeconds(1) + } + downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): vendorList]) + + and: "PBS with proper warmed older GVL config" + def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) + warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION - 1) + + and: "Valid tcf full enforcement requirments for older gvl list" def requirement = new EnforcementRequirement(purpose: P2, restrictionType: [REQUIRE_CONSENT, UNDEFINED], vendorIdGvl: GENERIC_VENDOR_ID, enforcePurpose: FULL, vendorConsentBitField: GENERIC_VENDOR_ID, enforceVendor: true, - vendorListVersion: vendorListVersion) + disclosedVendorsId: [GENERIC_VENDOR_ID], + vendorListVersion: VENDOR_LIST_VERSION - 1) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1198,58 +1295,62 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) accountDao.save(account) - and: "Set vendor list response with passed delete date for bidder" - def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { - deletedDate = ZonedDateTime.now().minusSeconds(1) - } - downloadAndWarmupGvtList(vendorListVersion, [(GENERIC_VENDOR_ID): vendorList]) - and: "Flush metric" - flushMetrics(privacyPbsService) + flushMetrics(pbsWithLiveGvlSetup) when: "PBS processes auction request" - def response = privacyPbsService.sendAuctionRequest(bidRequest) - - then: "Response should contain seatBid" - assert response.seatbid.bid.flatten().size() == 1 + def response = pbsWithLiveGvlSetup.sendAuctionRequest(bidRequest) - "Response shouldn't contain any nrb, errors and warnings" + then: "Response shouldn't contain any seatBids, errors and warnings" assert verifyAll(response) { - !it.noBidResponse + !it.seatbid.size() !it.ext.warnings !it.ext.errors } - and: "Bidder request should be valid" - assert bidder.getBidderRequests(bidRequest.id) + and: "Response should include nbr" + assert response.noBidResponse == NoBidResponse.UNKNOWN_ERROR - and: "Disallowed metrics shouldn't be updated" - def metrics = privacyPbsService.sendCollectedMetricsRequest() - assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 0 + and: "PBS should cansel request" + assert !bidder.getBidderRequests(bidRequest.id) - where: - gvlDeletedDate << [null, ZonedDateTime.now().plusYears(1)] - } + and: "Disallowed metrics should be updated" + def metrics = pbsWithLiveGvlSetup.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 - def "PBS should treated GVL record as invalid when it's marked as deleted for newer GVL file"() { - given: "Default PBS service with privacy" - def privacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(REFRESH_LIVE_VENDOR_LIST_CONFIG) + } - and: "Set latest vendor list response with passed delete date for bidder" - def vendorListVersion = PBSUtils.getRandomNumber(2, 4095) - def latestListWithExpiredVendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + def "PBS should treated GVL record as invalid when it's marked as deleted for older GVL file"() { + given: "Old vendor list response with passed delete date for bidder" + def olderVendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().minusSeconds(1) } - downloadAndWarmupGvtList(vendorListVersion, [(GENERIC_VENDOR_ID): latestListWithExpiredVendor]) + downloadGvtList(VENDOR_LIST_VERSION - 1, [(GENERIC_VENDOR_ID): olderVendorList]) - and: "Valid tcf full enforcement requirments" + and: "PBS with a high-frequency live GVL refresh config" + def config = REFRESH_LIVE_VENDOR_LIST_CONFIG + ['gdpr.vendorlist.live-gvl-refresh-period-ms': '1000'] + def pbsWithLiveGvlSetup = pbsServiceFactory.getService(config) + + and: "Newer vendor list where the bidder is not deleted yet" + def newerVendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = ZonedDateTime.now().plusYears(1) + } + downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): newerVendorList]) + + and: "GVL list is warmed up for PBS" + warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION) + + and: "Valid tcf full enforcement requirments for older GVL list" def requirement = new EnforcementRequirement(purpose: P2, restrictionType: [REQUIRE_CONSENT, UNDEFINED], vendorIdGvl: GENERIC_VENDOR_ID, enforcePurpose: FULL, vendorConsentBitField: GENERIC_VENDOR_ID, enforceVendor: true, - vendorListVersion: vendorListVersion - 1) + disclosedVendorsId: [GENERIC_VENDOR_ID], + vendorListVersion: VENDOR_LIST_VERSION) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1262,15 +1363,11 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) accountDao.save(account) - and: "Set older vendor list response with unexpired bidder" - def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID) - downloadAndWarmupGvtList(vendorListVersion - 1, [(GENERIC_VENDOR_ID): vendorList]) - and: "Flush metric" - flushMetrics(privacyPbsService) + flushMetrics(pbsWithLiveGvlSetup) when: "PBS processes auction request" - def response = privacyPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithLiveGvlSetup.sendAuctionRequest(bidRequest) then: "Response shouldn't contain any seatBids, errors and warnings" assert verifyAll(response) { @@ -1286,10 +1383,29 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert !bidder.getBidderRequests(bidRequest.id) and: "Disallowed metrics should be updated" - def metrics = privacyPbsService.sendCollectedMetricsRequest() + def metrics = pbsWithLiveGvlSetup.sendCollectedMetricsRequest() assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 cleanup: "Stop and remove pbs container" - pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + pbsServiceFactory.removeContainer(config) + } + + private static void downloadGvtList( + Integer vendorListVersion = VENDOR_LIST_VERSION, + Map vendors = [(GENERIC_VENDOR_ID): VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID)], + TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V5) { + + liveVendorListResponse.reset() + liveVendorListResponse.setResponse(tcfPolicyVersion, null, vendors, vendorListVersion) + } + + private static void warmupGvtList(PrebidServerService pbsService, Integer vendorListVersion = VENDOR_LIST_VERSION) { + def simpleTcfString = new TcfConsent.Builder() + .setDisclosedVendors([GENERIC_VENDOR_ID]) + .setVendorListVersion(vendorListVersion) + .build() + def bidRequest = getGdprBidRequest(simpleTcfString) + pbsService.sendAuctionRequest(bidRequest) + } } From 7f94d6e980866744911b9f02be019e3161c0491b Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Mon, 15 Jun 2026 13:26:23 +0300 Subject: [PATCH 3/4] update handle gvl deletions tests --- .../tests/privacy/gdpr/GdprAuctionSpec.groovy | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy index 68942f587ee..2193f5dd61a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy @@ -23,6 +23,7 @@ import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.functional.util.privacy.TcfUtils import org.prebid.server.functional.util.privacy.VendorListConsent +import spock.lang.IgnoreRest import spock.lang.PendingFeature import java.time.Instant @@ -1147,7 +1148,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = gvlDeletedDate } - downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): vendorList]) + downloadGvtList(VENDOR_LIST_VERSION, vendorList) and: "PBS with proper warmed GVL config" def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) @@ -1206,14 +1207,17 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def "PBS should treated GVL record as invalid when it's marked as deleted for GVL file"() { given: "Vendor list response with passed delete date for bidder" - def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { - deletedDate = ZonedDateTime.now().minusSeconds(1) + def vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = liveGvlDeletedDate } - downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): vendorList]) + downloadGvtList(VENDOR_LIST_VERSION, vendor) and: "PBS with proper warmed GVL config" def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) - warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION) + def requestVendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { + deletedDate = requestGvlDeletedDate + } + warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION, requestVendor) and: "Valid tcf full enforcement requirments" def requirement = new EnforcementRequirement(purpose: P2, @@ -1261,6 +1265,11 @@ class GdprAuctionSpec extends PrivacyBaseSpec { cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(REFRESH_LIVE_VENDOR_LIST_CONFIG) + + where: + liveGvlDeletedDate | requestGvlDeletedDate + ZonedDateTime.now().minusSeconds(1) | ZonedDateTime.now().plusYears(1) + ZonedDateTime.now().plusYears(1) | ZonedDateTime.now().minusSeconds(1) } def "PBS should treated GVL record as invalid when it's marked as deleted for newer GVL file"() { @@ -1268,7 +1277,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().minusSeconds(1) } - downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): vendorList]) + downloadGvtList(VENDOR_LIST_VERSION, vendorList) and: "PBS with proper warmed older GVL config" def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) @@ -1322,12 +1331,12 @@ class GdprAuctionSpec extends PrivacyBaseSpec { pbsServiceFactory.removeContainer(REFRESH_LIVE_VENDOR_LIST_CONFIG) } - def "PBS should treated GVL record as invalid when it's marked as deleted for older GVL file"() { + def "PBS should treat vendor as valid when deleted only in intermediate GVL versions"() { given: "Old vendor list response with passed delete date for bidder" def olderVendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().minusSeconds(1) } - downloadGvtList(VENDOR_LIST_VERSION - 1, [(GENERIC_VENDOR_ID): olderVendorList]) + downloadGvtList(VENDOR_LIST_VERSION, olderVendorList) and: "PBS with a high-frequency live GVL refresh config" def config = REFRESH_LIVE_VENDOR_LIST_CONFIG + ['gdpr.vendorlist.live-gvl-refresh-period-ms': '1000'] @@ -1337,10 +1346,10 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def newerVendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().plusYears(1) } - downloadGvtList(VENDOR_LIST_VERSION, [(GENERIC_VENDOR_ID): newerVendorList]) + downloadGvtList(VENDOR_LIST_VERSION + 1, newerVendorList) and: "GVL list is warmed up for PBS" - warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION) + warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION - 1) and: "Valid tcf full enforcement requirments for older GVL list" def requirement = new EnforcementRequirement(purpose: P2, @@ -1350,7 +1359,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { vendorConsentBitField: GENERIC_VENDOR_ID, enforceVendor: true, disclosedVendorsId: [GENERIC_VENDOR_ID], - vendorListVersion: VENDOR_LIST_VERSION) + vendorListVersion: VENDOR_LIST_VERSION - 1) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1369,22 +1378,22 @@ class GdprAuctionSpec extends PrivacyBaseSpec { when: "PBS processes auction request" def response = pbsWithLiveGvlSetup.sendAuctionRequest(bidRequest) - then: "Response shouldn't contain any seatBids, errors and warnings" + then: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + "Response shouldn't contain any nrb, errors and warnings" assert verifyAll(response) { - !it.seatbid.size() + !it.noBidResponse !it.ext.warnings !it.ext.errors } - and: "Response should include nbr" - assert response.noBidResponse == NoBidResponse.UNKNOWN_ERROR - - and: "PBS should cansel request" - assert !bidder.getBidderRequests(bidRequest.id) + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) - and: "Disallowed metrics should be updated" + and: "Disallowed metrics shouldn't be updated" def metrics = pbsWithLiveGvlSetup.sendCollectedMetricsRequest() - assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 0 cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(config) @@ -1392,18 +1401,26 @@ class GdprAuctionSpec extends PrivacyBaseSpec { private static void downloadGvtList( Integer vendorListVersion = VENDOR_LIST_VERSION, - Map vendors = [(GENERIC_VENDOR_ID): VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID)], + VendorListResponse.Vendor vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID), TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V5) { liveVendorListResponse.reset() - liveVendorListResponse.setResponse(tcfPolicyVersion, null, vendors, vendorListVersion) + liveVendorListResponse.setResponse(tcfPolicyVersion, null, [(vendor.id): vendor], vendorListVersion) } - private static void warmupGvtList(PrebidServerService pbsService, Integer vendorListVersion = VENDOR_LIST_VERSION) { + private static void warmupGvtList(PrebidServerService pbsService, + Integer vendorListVersion = VENDOR_LIST_VERSION, + VendorListResponse.Vendor vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID), + TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2 + ) { def simpleTcfString = new TcfConsent.Builder() .setDisclosedVendors([GENERIC_VENDOR_ID]) .setVendorListVersion(vendorListVersion) .build() + + vendorList.reset() + vendorList.setResponse(tcfPolicyVersion, null, [(vendor.id): vendor]) + def bidRequest = getGdprBidRequest(simpleTcfString) pbsService.sendAuctionRequest(bidRequest) From 4bc68dcd98e9db2feeac32bb966c1e1f017c1bdb Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 2 Jul 2026 22:29:44 +0300 Subject: [PATCH 4/4] fix flack for gvl test --- .../service/PrebidServerService.groovy | 9 +++ .../scaffolding/VendorList.groovy | 9 ++- .../tests/privacy/gdpr/GdprAuctionSpec.groovy | 73 ++++++------------- 3 files changed, 36 insertions(+), 55 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 249d6fa3f13..2e0d0765a0f 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -448,6 +448,15 @@ class PrebidServerService implements ObjectMapperWrapper { } } + Boolean isMetricFilled(String metricName) { + try { + PBSUtils.waitUntil({ this.sendCollectedMetricsRequest()[metricName] != 0 }) + true + } catch (IllegalStateException ignored) { + false + } + } + Boolean isContainMetricByValue(String value) { try { PBSUtils.waitUntil({ sendInfluxMetricsRequest()[value] != null }) 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 9e079f0bc02..29b0060af27 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 @@ -49,7 +49,8 @@ class VendorList extends NetworkScaffolding { void setResponse(TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, Delay delay = null, Map vendors = [(GENERIC_VENDOR_ID): Vendor.getDefaultVendor(GENERIC_VENDOR_ID)], - vendorListVersion = TcfConsent.VENDOR_LIST_VERSION) { + Integer vendorListVersion = TcfConsent.VENDOR_LIST_VERSION, + Times times = Times.unlimited()) { def prepareEndpoint = endpoint.replace("{TCF_POLICY}", tcfPolicyVersion.vendorListVersion.toString()) def prepareEncodeResponseBody = encode(defaultVendorListResponse.tap { @@ -63,12 +64,12 @@ class VendorList extends NetworkScaffolding { .withStatusCode(OK_200.code()) .withBody(prepareEncodeResponseBody) - if (delay != null) { - mockResponse.withDelay(delay) + delay?.with { + mockResponse.withDelay(it) } mockServerClient - .when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .when(request().withPath(prepareEndpoint), times, TimeToLive.unlimited(), -10) .respond(mockResponse) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy index 2193f5dd61a..8b5996089b3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/gdpr/GdprAuctionSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests.privacy.gdpr +import org.mockserver.matchers.Times import org.mockserver.model.Delay import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountGdprConfig @@ -23,7 +24,6 @@ import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.functional.util.privacy.TcfUtils import org.prebid.server.functional.util.privacy.VendorListConsent -import spock.lang.IgnoreRest import spock.lang.PendingFeature import java.time.Instant @@ -36,7 +36,6 @@ import static org.prebid.server.functional.model.config.AccountMetricsVerbosityL import static org.prebid.server.functional.model.config.Purpose.P1 import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 -import static org.prebid.server.functional.model.config.PurposeEnforcement.FULL import static org.prebid.server.functional.model.config.PurposeEnforcement.NO import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA @@ -58,8 +57,6 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS -import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.REQUIRE_CONSENT -import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.UNDEFINED import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V4 import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V5 @@ -1148,21 +1145,14 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = gvlDeletedDate } - downloadGvtList(VENDOR_LIST_VERSION, vendorList) + downloadLiveGvtList(VENDOR_LIST_VERSION, vendorList) and: "PBS with proper warmed GVL config" def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION) and: "Valid tcf full enforcement requirments" - def requirement = new EnforcementRequirement(purpose: P2, - restrictionType: [REQUIRE_CONSENT, UNDEFINED], - vendorIdGvl: GENERIC_VENDOR_ID, - enforcePurpose: FULL, - vendorConsentBitField: GENERIC_VENDOR_ID, - enforceVendor: true, - disclosedVendorsId: [GENERIC_VENDOR_ID], - vendorListVersion: VENDOR_LIST_VERSION) + def requirement = EnforcementRequirement.getDefaultFull(GENERIC_VENDOR_ID, VENDOR_LIST_VERSION, P2) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1196,7 +1186,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { and: "Disallowed metrics shouldn't be updated" def metrics = pbsWithLiveGvlSetup.sendCollectedMetricsRequest() - assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 0 + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(REFRESH_LIVE_VENDOR_LIST_CONFIG) @@ -1210,7 +1200,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = liveGvlDeletedDate } - downloadGvtList(VENDOR_LIST_VERSION, vendor) + downloadLiveGvtList(VENDOR_LIST_VERSION, vendor) and: "PBS with proper warmed GVL config" def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) @@ -1220,14 +1210,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION, requestVendor) and: "Valid tcf full enforcement requirments" - def requirement = new EnforcementRequirement(purpose: P2, - restrictionType: [REQUIRE_CONSENT, UNDEFINED], - vendorIdGvl: GENERIC_VENDOR_ID, - enforcePurpose: FULL, - vendorConsentBitField: GENERIC_VENDOR_ID, - enforceVendor: true, - disclosedVendorsId: [GENERIC_VENDOR_ID], - vendorListVersion: VENDOR_LIST_VERSION) + def requirement = EnforcementRequirement.getDefaultFull(GENERIC_VENDOR_ID, VENDOR_LIST_VERSION, P2) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1277,21 +1260,14 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def vendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().minusSeconds(1) } - downloadGvtList(VENDOR_LIST_VERSION, vendorList) + downloadLiveGvtList(VENDOR_LIST_VERSION, vendorList) and: "PBS with proper warmed older GVL config" def pbsWithLiveGvlSetup = pbsServiceFactory.getService(REFRESH_LIVE_VENDOR_LIST_CONFIG) warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION - 1) and: "Valid tcf full enforcement requirments for older gvl list" - def requirement = new EnforcementRequirement(purpose: P2, - restrictionType: [REQUIRE_CONSENT, UNDEFINED], - vendorIdGvl: GENERIC_VENDOR_ID, - enforcePurpose: FULL, - vendorConsentBitField: GENERIC_VENDOR_ID, - enforceVendor: true, - disclosedVendorsId: [GENERIC_VENDOR_ID], - vendorListVersion: VENDOR_LIST_VERSION - 1) + def requirement = EnforcementRequirement.getDefaultFull(GENERIC_VENDOR_ID, VENDOR_LIST_VERSION - 1, P2) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1336,30 +1312,27 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def olderVendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().minusSeconds(1) } - downloadGvtList(VENDOR_LIST_VERSION, olderVendorList) + downloadLiveGvtList(VENDOR_LIST_VERSION, olderVendorList, Times.once()) and: "PBS with a high-frequency live GVL refresh config" def config = REFRESH_LIVE_VENDOR_LIST_CONFIG + ['gdpr.vendorlist.live-gvl-refresh-period-ms': '1000'] def pbsWithLiveGvlSetup = pbsServiceFactory.getService(config) - and: "Newer vendor list where the bidder is not deleted yet" + and: "Flash metrics" + flushMetrics(pbsWithLiveGvlSetup) + + and: "Newer vendor list where bidder is not deleted yet" def newerVendorList = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID).tap { deletedDate = ZonedDateTime.now().plusYears(1) } - downloadGvtList(VENDOR_LIST_VERSION + 1, newerVendorList) + downloadLiveGvtList(VENDOR_LIST_VERSION + 1, newerVendorList, Times.once()) + pbsWithLiveGvlSetup.isContainMetricByValue('privacy.tcf.vendorlist.latest.ok') and: "GVL list is warmed up for PBS" warmupGvtList(pbsWithLiveGvlSetup, VENDOR_LIST_VERSION - 1) and: "Valid tcf full enforcement requirments for older GVL list" - def requirement = new EnforcementRequirement(purpose: P2, - restrictionType: [REQUIRE_CONSENT, UNDEFINED], - vendorIdGvl: GENERIC_VENDOR_ID, - enforcePurpose: FULL, - vendorConsentBitField: GENERIC_VENDOR_ID, - enforceVendor: true, - disclosedVendorsId: [GENERIC_VENDOR_ID], - vendorListVersion: VENDOR_LIST_VERSION - 1) + def requirement = EnforcementRequirement.getDefaultFull(GENERIC_VENDOR_ID, VENDOR_LIST_VERSION - 1, P2) and: "Tcf consent setup" def tcfConsent = TcfUtils.getConsentString(requirement) @@ -1399,30 +1372,28 @@ class GdprAuctionSpec extends PrivacyBaseSpec { pbsServiceFactory.removeContainer(config) } - private static void downloadGvtList( + private static void downloadLiveGvtList( Integer vendorListVersion = VENDOR_LIST_VERSION, VendorListResponse.Vendor vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID), - TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V5) { + Times times = Times.unlimited()) { liveVendorListResponse.reset() - liveVendorListResponse.setResponse(tcfPolicyVersion, null, [(vendor.id): vendor], vendorListVersion) + liveVendorListResponse.setResponse(TCF_POLICY_V5, null, [(vendor.id): vendor], vendorListVersion, times) } private static void warmupGvtList(PrebidServerService pbsService, Integer vendorListVersion = VENDOR_LIST_VERSION, - VendorListResponse.Vendor vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID), - TcfConsent.TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2 - ) { + VendorListResponse.Vendor vendor = VendorListResponse.Vendor.getDefaultVendor(GENERIC_VENDOR_ID)) { + def simpleTcfString = new TcfConsent.Builder() .setDisclosedVendors([GENERIC_VENDOR_ID]) .setVendorListVersion(vendorListVersion) .build() vendorList.reset() - vendorList.setResponse(tcfPolicyVersion, null, [(vendor.id): vendor]) + vendorList.setResponse(TCF_POLICY_V2, null, [(vendor.id): vendor]) def bidRequest = getGdprBidRequest(simpleTcfString) pbsService.sendAuctionRequest(bidRequest) - } }