From 3a0c16dbd115f07b3fad8769ac514f3ccd0731ac Mon Sep 17 00:00:00 2001 From: mtuchkova <47950518+mtuchkova@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:58:06 +0200 Subject: [PATCH] Rename integration to channel in account config tests (#1605) --- .../model/{config => }/ChannelType.groovy | 2 +- .../model/config/AccountCcpaConfig.groovy | 3 +- .../model/config/AccountGdprConfig.groovy | 3 +- .../model/request/amp/AmpRequest.groovy | 1 - .../model/request/auction/App.groovy | 5 +- .../model/request/auction/BidRequest.groovy | 12 +- .../model/request/auction/Channel.groovy | 4 +- .../auction/DistributionChannel.groovy | 13 ++ .../model/request/auction/UserExt.groovy | 2 +- .../testcontainers/PbsServiceFactory.groovy | 10 +- .../container/PrebidServerContainer.groovy | 4 +- .../functional/tests/pg/TargetingSpec.groovy | 5 +- .../{CcpaSpec.groovy => CcpaAmpSpec.groovy} | 168 ++++++-------- .../tests/privacy/CcpaAuctionSpec.groovy | 208 ++++++++++++++++++ .../functional/tests/privacy/CoppaSpec.groovy | 11 +- .../{GdprSpec.groovy => GdprAmpSpec.groovy} | 171 +++++++------- .../tests/privacy/GdprAuctionSpec.groovy | 202 +++++++++++++++++ .../tests/privacy/PrivacyBaseSpec.groovy | 44 +++- .../functional/util/privacy/TcfConsent.groovy | 7 + 19 files changed, 657 insertions(+), 218 deletions(-) rename src/test/groovy/org/prebid/server/functional/model/{config => }/ChannelType.groovy (77%) create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DistributionChannel.groovy rename src/test/groovy/org/prebid/server/functional/tests/privacy/{CcpaSpec.groovy => CcpaAmpSpec.groovy} (63%) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy rename src/test/groovy/org/prebid/server/functional/tests/privacy/{GdprSpec.groovy => GdprAmpSpec.groovy} (64%) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ChannelType.groovy b/src/test/groovy/org/prebid/server/functional/model/ChannelType.groovy similarity index 77% rename from src/test/groovy/org/prebid/server/functional/model/config/ChannelType.groovy rename to src/test/groovy/org/prebid/server/functional/model/ChannelType.groovy index f01abb7800a..e6df4510b97 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ChannelType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ChannelType.groovy @@ -1,4 +1,4 @@ -package org.prebid.server.functional.model.config +package org.prebid.server.functional.model import com.fasterxml.jackson.annotation.JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy index 155889280f6..52530fef872 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy @@ -3,11 +3,12 @@ package org.prebid.server.functional.model.config import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.ChannelType @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountCcpaConfig { Boolean enabled - Map enabledForRequestType + Map channelEnabled } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy index 850c95aa2c2..ba1ec11c608 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy @@ -3,13 +3,14 @@ package org.prebid.server.functional.model.config import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.ChannelType @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountGdprConfig { Boolean enabled - Map enabledForRequestType + Map channelEnabled Map purposes Map specialFeatures PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation diff --git a/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy index 99aeb50d2c7..2e7be12e71b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy @@ -22,7 +22,6 @@ class AmpRequest { Integer account String gdprConsent String consentString - String targeting ConsentType consentType Boolean gdprApplies String addtlConsent diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy index afe7d82a65e..be00430397b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy @@ -23,6 +23,9 @@ class App { AppExt ext static App getDefaultApp() { - new App(id: PBSUtils.randomString) + new App().tap { + id = PBSUtils.randomString + publisher = Publisher.defaultPublisher + } } } 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 04d0dcc0d70..d29a530fbe6 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 @@ -4,6 +4,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE + @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class BidRequest { @@ -29,14 +32,19 @@ class BidRequest { Regs regs BidRequestExt ext - static BidRequest getDefaultBidRequest() { + static BidRequest getDefaultBidRequest(DistributionChannel channel = SITE) { new BidRequest().tap { it.addImp(Imp.defaultImpression) regs = Regs.defaultRegs id = UUID.randomUUID() tmax = 2500 - site = Site.defaultSite ext = new BidRequestExt(prebid: new Prebid(debug: 1)) + if (channel == SITE) { + site = Site.defaultSite + } + if (channel == APP) { + app = App.defaultApp + } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Channel.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Channel.groovy index 551d94a40c2..51c504c6822 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Channel.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Channel.groovy @@ -1,9 +1,11 @@ package org.prebid.server.functional.model.request.auction import groovy.transform.ToString +import org.prebid.server.functional.model.ChannelType @ToString(includeNames = true, ignoreNulls = true) class Channel { - String name + ChannelType name + String version } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DistributionChannel.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DistributionChannel.groovy new file mode 100644 index 00000000000..e7ecc064136 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DistributionChannel.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum DistributionChannel { + + SITE, APP + + @JsonValue + String getValue() { + name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy index 9e51f41fd18..4a2dece11df 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy @@ -6,7 +6,7 @@ import org.prebid.server.functional.util.privacy.ConsentString @ToString(includeNames = true, ignoreNulls = true) class UserExt { - ConsentString consent + String consent List fcapids UserTime time UserExtData data 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 5d7e405a66e..dcc3606d49d 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy @@ -22,13 +22,13 @@ class PbsServiceFactory { } PrebidServerService getService(Map config) { - if (containers.size() >= MAX_CONTAINERS_COUNT) { - def container = containers.find { !it.key.isEmpty() } - remove([(container.key): container.value]) - } if (containers.containsKey(config)) { - return new PrebidServerService(getContainer(config), mapper) + return new PrebidServerService(containers.get(config), mapper) } else { + if (containers.size() >= MAX_CONTAINERS_COUNT) { + def container = containers.find { !it.key.isEmpty() } + remove([(container.key): container.value]) + } def pbsContainer = new PrebidServerContainer(config) pbsContainer.start() containers.put(config, pbsContainer) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index ca857b48671..18698744d77 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -115,8 +115,8 @@ LIMIT 1 void withBidder(String host) { withConfig(["adapters.generic.enabled" : "true", - "adapters.generic.endpoint" : "$host/auction".toString(), - "adapters.generic.usersync.url" : "$host/generic-usersync".toString(), + "adapters.generic.endpoint" : "$host/auction" as String, + "adapters.generic.usersync.url" : "$host/generic-usersync" as String, "adapters.generic.usersync.type": "redirect" ]) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy index 9c12fee7420..03996964936 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy @@ -207,7 +207,8 @@ class TargetingSpec extends BasePgSpec { } APP_BUNDLE | BidRequest.defaultBidRequest.tap { - app = App.defaultApp.tap { bundle = stringTargetingValue } + app = new App(id: PBSUtils.randomString, + bundle: stringTargetingValue) } UFPD_LANGUAGE | BidRequest.defaultBidRequest.tap { @@ -320,7 +321,7 @@ class TargetingSpec extends BasePgSpec { } }, BidRequest.defaultBidRequest.tap { - app = App.defaultApp.tap { + app = new App(id: PBSUtils.randomString).tap { ext = new AppExt(data: new AppExtData(language: stringTargetingValue)) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy similarity index 63% rename from src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaSpec.groovy rename to src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy index b2cdab132dd..21273aa0fb5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy @@ -1,6 +1,5 @@ package org.prebid.server.functional.tests.privacy -import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountCcpaConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountPrivacyConfig @@ -13,65 +12,30 @@ import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.CcpaConsent import org.prebid.server.functional.util.privacy.TcfConsent import spock.lang.PendingFeature +import spock.lang.Unroll +import static org.prebid.server.functional.model.ChannelType.AMP +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS @PBSTest -class CcpaSpec extends PrivacyBaseSpec { - - // TODO: extend ccpa test with actual fields that we should mask - def "PBS should mask publisher info when privacy.ccpa.enabled = true in account config"() { - given: "Default ccpa BidRequest" - def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - def bidRequest = getCcpaBidRequest(validCcpa) - - and: "Save account config into DB" - def ccpa = new AccountCcpaConfig(enabled: true) - def privacy = new AccountPrivacyConfig(ccpa: ccpa) - def accountConfig = new AccountConfig(privacy: privacy) - def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) - accountDao.save(account) - - when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder request should contain masked values" - def bidderRequests = bidder.getBidderRequest(bidRequest.id) - assert bidderRequests.device?.geo == maskGeo(bidRequest) - } - - // TODO: extend this ccpa test with actual fields that we should mask - def "PBS should not mask publisher info when privacy.ccpa.enabled = false in account config"() { - given: "Default ccpa BidRequest" - def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - def bidRequest = getCcpaBidRequest(validCcpa) - - and: "Save account config into DB" - def ccpa = new AccountCcpaConfig(enabled: false) - def privacy = new AccountPrivacyConfig(ccpa: ccpa) - def accountConfig = new AccountConfig(privacy: privacy) - def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) - accountDao.save(account) - - when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder request should contain not masked values" - def bidderRequests = bidder.getBidderRequest(bidRequest.id) - assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat - assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon - } +class CcpaAmpSpec extends PrivacyBaseSpec { @PendingFeature - def "PBS should add debug log for auction request when valid ccpa was passed"() { - given: "Default ccpa BidRequest" + def "PBS should add debug log for amp request when valid ccpa was passed"() { + given: "Default AmpRequest" def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - def bidRequest = getCcpaBidRequest(validCcpa) + def ampRequest = getCcpaAmpRequest(validCcpa) + + and: "Save storedRequest into DB" + def ampStoredRequest = storedRequestWithGeo + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) then: "Response should contain debug log" assert response.ext?.debug?.privacy @@ -93,7 +57,7 @@ class CcpaSpec extends PrivacyBaseSpec { !privacy.resolvedPrivacy?.tcf?.tcfConsentVersion !privacy.resolvedPrivacy?.tcf?.inEea - privacy.privacyActionsPerBidder[BidderName.GENERIC] == + privacy.privacyActionsPerBidder[GENERIC] == ["Geolocation was masked in request to bidder according to CCPA policy."] privacy.errors?.isEmpty() @@ -101,13 +65,18 @@ class CcpaSpec extends PrivacyBaseSpec { } @PendingFeature - def "PBS should add debug log for auction request when invalid ccpa was passed"() { - given: "Default ccpa BidRequest" + def "PBS should add debug log for amp request when invalid ccpa was passed"() { + given: "Default AmpRequest" def invalidCcpa = new BogusConsent() - def bidRequest = getCcpaBidRequest(invalidCcpa) + def ampRequest = getCcpaAmpRequest(invalidCcpa) + + and: "Save storedRequest into DB" + def ampStoredRequest = storedRequestWithGeo + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) then: "Response should not contain error" assert !response.ext?.errors @@ -120,81 +89,70 @@ class CcpaSpec extends PrivacyBaseSpec { privacy.originPrivacy?.ccpa?.usPrivacy == invalidCcpa as String privacy.resolvedPrivacy?.ccpa?.usPrivacy == invalidCcpa as String - privacy.privacyActionsPerBidder[BidderName.GENERIC].isEmpty() + privacy.privacyActionsPerBidder[GENERIC].isEmpty() - privacy.errors == ["CCPA consent $invalidCcpa has invalid format: us_privacy must specify 'N' " + - "or 'n', 'Y' or 'y', '-' for the explicit notice" as String] + privacy.errors == + ["Amp request parameter consent_string or gdpr_consent have invalid format: $invalidCcpa" as String] } } - @PendingFeature - def "PBS should add debug log for amp request when valid ccpa was passed"() { + @Unroll + def "PBS should apply ccpa when privacy.ccpa.channel-enabled.amp or privacy.ccpa.enabled = true in account config"() { given: "Default AmpRequest" def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) def ampRequest = getCcpaAmpRequest(validCcpa) and: "Save storedRequest into DB" - def ampStoredRequest = bidRequestWithGeo + def ampStoredRequest = storedRequestWithGeo def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) - when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) - - then: "Response should contain debug log" - assert response.ext?.debug?.privacy - def privacy = response.ext?.debug?.privacy - - verifyAll { - privacy.originPrivacy?.ccpa?.usPrivacy == validCcpa as String - privacy.resolvedPrivacy?.ccpa?.usPrivacy == validCcpa as String - - !privacy.originPrivacy?.coppa?.coppa - !privacy.resolvedPrivacy?.coppa?.coppa + and: "Save account config into DB" + def privacy = new AccountPrivacyConfig(ccpa: ccpaConfig) + def accountConfig = new AccountConfig(privacy: privacy) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) - !privacy.originPrivacy?.tcf?.gdpr - !privacy.originPrivacy?.tcf?.tcfConsentString - !privacy.originPrivacy?.tcf?.tcfConsentVersion - !privacy.originPrivacy?.tcf?.inEea - !privacy.resolvedPrivacy?.tcf?.gdpr - !privacy.resolvedPrivacy?.tcf?.tcfConsentString - !privacy.resolvedPrivacy?.tcf?.tcfConsentVersion - !privacy.resolvedPrivacy?.tcf?.inEea + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) - privacy.privacyActionsPerBidder[BidderName.GENERIC] == - ["Geolocation was masked in request to bidder according to CCPA policy."] + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequests.device?.geo == maskGeo(ampStoredRequest) - privacy.errors?.isEmpty() - } + where: + ccpaConfig << [new AccountCcpaConfig(enabled: false, channelEnabled: [(AMP): true]), + new AccountCcpaConfig(enabled: true)] } - @PendingFeature - def "PBS should add debug log for amp request when invalid ccpa was passed"() { + @Unroll + def "PBS should not apply ccpa when privacy.ccpa.channel-enabled.amp or privacy.ccpa.enabled = false in account config"() { given: "Default AmpRequest" - def invalidCcpa = new BogusConsent() - def ampRequest = getCcpaAmpRequest(invalidCcpa) + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def ampRequest = getCcpaAmpRequest(validCcpa) and: "Save storedRequest into DB" - def ampStoredRequest = bidRequestWithGeo + def ampStoredRequest = storedRequestWithGeo def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) - when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) - - then: "Response should contain debug log with error" - assert response.ext?.debug?.privacy - def privacy = response.ext?.debug?.privacy + and: "Save account config into DB" + def privacy = new AccountPrivacyConfig(ccpa: ccpaConfig) + def accountConfig = new AccountConfig(privacy: privacy) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) - verifyAll { - privacy.originPrivacy?.ccpa?.usPrivacy == invalidCcpa as String - privacy.resolvedPrivacy?.ccpa?.usPrivacy == invalidCcpa as String + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) - privacy.privacyActionsPerBidder[BidderName.GENERIC].isEmpty() + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequests.device?.geo?.lat == ampStoredRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == ampStoredRequest.device.geo.lon - privacy.errors == - ["Amp request parameter consent_string or gdpr_consent have invalid format: $invalidCcpa" as String] - } + where: + ccpaConfig << [new AccountCcpaConfig(enabled: true, channelEnabled: [(AMP): false]), + new AccountCcpaConfig(enabled: false)] } def "PBS should emit error for amp request when consent_string contains invalid ccpa consent"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy new file mode 100644 index 00000000000..4d067111a37 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy @@ -0,0 +1,208 @@ +package org.prebid.server.functional.tests.privacy + +import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.config.AccountCcpaConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.testcontainers.PBSTest +import org.prebid.server.functional.util.privacy.BogusConsent +import org.prebid.server.functional.util.privacy.CcpaConsent +import spock.lang.PendingFeature + +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.util.privacy.CcpaConsent.Signal.ENFORCED + +@PBSTest +class CcpaAuctionSpec extends PrivacyBaseSpec { + + // TODO: extend ccpa test with actual fields that we should mask + def "PBS should mask publisher info when privacy.ccpa.enabled = true in account config"() { + given: "Default ccpa BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(validCcpa) + + and: "Save account config into DB" + def ccpaConfig = new AccountCcpaConfig(enabled: true) + accountDao.save(getAccountWithCcpa(bidRequest.site.publisher.id, ccpaConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo == maskGeo(bidRequest) + } + + // TODO: extend this ccpa test with actual fields that we should mask + def "PBS should not mask publisher info when privacy.ccpa.enabled = false in account config"() { + given: "Default ccpa BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(validCcpa) + + and: "Save account config into DB" + def ccpaConfig = new AccountCcpaConfig(enabled: false) + accountDao.save(getAccountWithCcpa(bidRequest.site.publisher.id, ccpaConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon + } + + @PendingFeature + def "PBS should add debug log for auction request when valid ccpa was passed"() { + given: "Default ccpa BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(validCcpa) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain debug log" + assert response.ext?.debug?.privacy + def privacy = response.ext?.debug?.privacy + + verifyAll { + privacy.originPrivacy?.ccpa?.usPrivacy == validCcpa as String + privacy.resolvedPrivacy?.ccpa?.usPrivacy == validCcpa as String + + !privacy.originPrivacy?.coppa?.coppa + !privacy.resolvedPrivacy?.coppa?.coppa + + !privacy.originPrivacy?.tcf?.gdpr + !privacy.originPrivacy?.tcf?.tcfConsentString + !privacy.originPrivacy?.tcf?.tcfConsentVersion + !privacy.originPrivacy?.tcf?.inEea + !privacy.resolvedPrivacy?.tcf?.gdpr + !privacy.resolvedPrivacy?.tcf?.tcfConsentString + !privacy.resolvedPrivacy?.tcf?.tcfConsentVersion + !privacy.resolvedPrivacy?.tcf?.inEea + + privacy.privacyActionsPerBidder[GENERIC] == + ["Geolocation was masked in request to bidder according to CCPA policy."] + + privacy.errors?.isEmpty() + } + } + + @PendingFeature + def "PBS should add debug log for auction request when invalid ccpa was passed"() { + given: "Default ccpa BidRequest" + def invalidCcpa = new BogusConsent() + def bidRequest = getCcpaBidRequest(invalidCcpa) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should not contain error" + assert !response.ext?.errors + + and: "Response should contain debug log with error" + assert response.ext?.debug?.privacy + def privacy = response.ext?.debug?.privacy + + verifyAll { + privacy.originPrivacy?.ccpa?.usPrivacy == invalidCcpa as String + privacy.resolvedPrivacy?.ccpa?.usPrivacy == invalidCcpa as String + + privacy.privacyActionsPerBidder[GENERIC].isEmpty() + + privacy.errors == ["CCPA consent $invalidCcpa has invalid format: us_privacy must specify 'N' " + + "or 'n', 'Y' or 'y', '-' for the explicit notice" as String] + } + } + + def "PBS should apply ccpa when privacy.ccpa.channel-enabled.app or privacy.ccpa.enabled = true in account config"() { + given: "Default basic generic BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(DistributionChannel.APP, validCcpa) + + and: "Save account config into DB" + accountDao.save(getAccountWithCcpa(bidRequest.app.publisher.id, ccpaConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo == maskGeo(bidRequest) + + where: + ccpaConfig << [new AccountCcpaConfig(enabled: false, channelEnabled: [(ChannelType.APP): true]), + new AccountCcpaConfig(enabled: true)] + } + + def "PBS should apply ccpa when privacy.ccpa.channel-enabled.web or privacy.ccpa.enabled = true in account config"() { + given: "Default basic generic BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(validCcpa) + + and: "Save account config into DB" + accountDao.save(getAccountWithCcpa(bidRequest.site.publisher.id, ccpaConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo == maskGeo(bidRequest) + + where: + ccpaConfig << [new AccountCcpaConfig(enabled: false, channelEnabled: [(WEB): true]), + new AccountCcpaConfig(enabled: true)] + } + + def "PBS should not apply ccpa when privacy.ccpa.channel-enabled.app or privacy.ccpa.enabled = false in account config"() { + given: "Default basic generic BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(DistributionChannel.APP, validCcpa) + + and: "Save account config into DB" + accountDao.save(getAccountWithCcpa(bidRequest.app.publisher.id, ccpaConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon + + where: + ccpaConfig << [new AccountCcpaConfig(enabled: true, channelEnabled: [(ChannelType.APP): false]), + new AccountCcpaConfig(enabled: false)] + } + + def "PBS should not apply ccpa when privacy.ccpa.channel-enabled.web or privacy.ccpa.enabled = false in account config"() { + given: "Default basic generic BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(validCcpa) + + and: "Save account config into DB" + accountDao.save(getAccountWithCcpa(bidRequest.site.publisher.id, ccpaConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon + + where: + ccpaConfig << [new AccountCcpaConfig(enabled: true, channelEnabled: [(WEB): false]), + new AccountCcpaConfig(enabled: false)] + } + + private Account getAccountWithCcpa(String accountId, AccountCcpaConfig ccpaConfig) { + def privacy = new AccountPrivacyConfig(ccpa: ccpaConfig) + def accountConfig = new AccountConfig(privacy: privacy) + new Account(uuid: accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy index a41d40b903f..2ef997c0e2b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy @@ -1,11 +1,12 @@ package org.prebid.server.functional.tests.privacy -import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.testcontainers.PBSTest import spock.lang.PendingFeature +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC + @PBSTest class CoppaSpec extends PrivacyBaseSpec { @@ -39,7 +40,7 @@ class CoppaSpec extends PrivacyBaseSpec { !privacy.originPrivacy?.ccpa?.usPrivacy !privacy.resolvedPrivacy?.ccpa?.usPrivacy - privacy.privacyActionsPerBidder[BidderName.GENERIC].isEmpty() + privacy.privacyActionsPerBidder[GENERIC].isEmpty() privacy.errors?.isEmpty() } @@ -63,7 +64,7 @@ class CoppaSpec extends PrivacyBaseSpec { privacy.originPrivacy?.coppa?.coppa == bidRequest.regs.coppa privacy.resolvedPrivacy?.coppa?.coppa == bidRequest.regs.coppa - privacy.privacyActionsPerBidder[BidderName.GENERIC] == + privacy.privacyActionsPerBidder[GENERIC] == ["Geolocation and address were removed from request to bidder according to CCPA policy."] privacy.errors?.isEmpty() @@ -105,7 +106,7 @@ class CoppaSpec extends PrivacyBaseSpec { !privacy.originPrivacy?.ccpa?.usPrivacy !privacy.resolvedPrivacy?.ccpa?.usPrivacy - privacy.privacyActionsPerBidder[BidderName.GENERIC].isEmpty() + privacy.privacyActionsPerBidder[GENERIC].isEmpty() privacy.errors?.isEmpty() } @@ -134,7 +135,7 @@ class CoppaSpec extends PrivacyBaseSpec { privacy.originPrivacy?.coppa?.coppa == ampStoredRequest.regs.coppa privacy.resolvedPrivacy?.coppa?.coppa == ampStoredRequest.regs.coppa - privacy.privacyActionsPerBidder[BidderName.GENERIC] == + privacy.privacyActionsPerBidder[GENERIC] == ["Geolocation and address were removed from request to bidder according to CCPA policy."] privacy.errors?.isEmpty() diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy similarity index 64% rename from src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSpec.groovy rename to src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy index 09573d3453b..3a2ad1fd77d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy @@ -1,92 +1,31 @@ package org.prebid.server.functional.tests.privacy -import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountGdprConfig +import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.testcontainers.PBSTest import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.CcpaConsent import org.prebid.server.functional.util.privacy.TcfConsent import spock.lang.PendingFeature +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS import static org.prebid.server.functional.model.request.amp.ConsentType.TCF_1 +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS @PBSTest -class GdprSpec extends PrivacyBaseSpec { +class GdprAmpSpec extends PrivacyBaseSpec { - @PendingFeature - def "PBS should add debug log for auction request when valid gdpr was passed"() { - given: "Default gdpr BidRequest" - def validConsentString = new TcfConsent.Builder() - .setPurposesLITransparency(BASIC_ADS) - .build() - - def bidRequest = getGdprBidRequest(validConsentString) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Response should contain debug log" - assert response.ext?.debug?.privacy - def privacy = response.ext?.debug?.privacy - - verifyAll { - privacy.originPrivacy?.tcf?.gdpr == "1" - privacy.originPrivacy?.tcf?.tcfConsentString == validConsentString as String - !privacy.originPrivacy?.tcf?.tcfConsentVersion - !privacy.originPrivacy?.tcf?.inEea - privacy.resolvedPrivacy?.tcf?.gdpr == "1" - privacy.resolvedPrivacy?.tcf?.tcfConsentString == validConsentString as String - privacy.resolvedPrivacy?.tcf?.tcfConsentVersion == 2 - !privacy.resolvedPrivacy?.tcf?.inEea - - !privacy.originPrivacy?.ccpa?.usPrivacy - !privacy.resolvedPrivacy?.ccpa?.usPrivacy - - !privacy.originPrivacy?.coppa?.coppa - privacy.resolvedPrivacy?.coppa?.coppa == 0 - - privacy.privacyActionsPerBidder[BidderName.GENERIC] == - ["Geolocation was masked in request to bidder according to TCF policy."] - - privacy.errors?.isEmpty() - } - } - - @PendingFeature - def "PBS should add debug log for auction request when invalid gdpr was passed"() { - given: "Default gdpr BidRequest" - def invalidConsentString = new BogusConsent() - def bidRequest = getGdprBidRequest(invalidConsentString) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Response should not contain ext.errors" - assert !response.ext?.errors - - and: "Response should contain debug log with error" - assert response.ext?.debug?.privacy - def privacy = response.ext?.debug?.privacy - - verifyAll { - privacy.originPrivacy?.tcf?.gdpr == "1" - privacy.originPrivacy?.tcf?.tcfConsentString == invalidConsentString as String - !privacy.originPrivacy?.tcf?.tcfConsentVersion - !privacy.originPrivacy?.tcf?.inEea - privacy.resolvedPrivacy?.tcf?.gdpr == "1" - privacy.resolvedPrivacy?.tcf?.tcfConsentString == invalidConsentString as String - privacy.resolvedPrivacy?.tcf?.tcfConsentVersion == 2 - !privacy.resolvedPrivacy?.tcf?.inEea - - privacy.privacyActionsPerBidder[BidderName.GENERIC].isEmpty() - - privacy.errors == ["Placeholder: invalid consent string"] - } + def setupSpec() { + cacheVendorList() } @PendingFeature @@ -94,12 +33,13 @@ class GdprSpec extends PrivacyBaseSpec { given: "AmpRequest with consent string" def validConsentString = new TcfConsent.Builder() .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() def ampRequest = getGdprAmpRequest(validConsentString) and: "Save storedRequest into DB" - def ampStoredRequest = bidRequestWithGeo + def ampStoredRequest = storedRequestWithGeo def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -126,7 +66,7 @@ class GdprSpec extends PrivacyBaseSpec { !privacy.originPrivacy?.coppa?.coppa privacy.resolvedPrivacy?.coppa?.coppa == 0 - privacy.privacyActionsPerBidder[BidderName.GENERIC] == + privacy.privacyActionsPerBidder[GENERIC] == ["Geolocation was masked in request to bidder according to TCF policy."] privacy.errors?.isEmpty() @@ -140,7 +80,7 @@ class GdprSpec extends PrivacyBaseSpec { def ampRequest = getGdprAmpRequest(invalidConsentString) and: "Save storedRequest into DB" - def ampStoredRequest = bidRequestWithGeo + def ampStoredRequest = storedRequestWithGeo def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -161,7 +101,7 @@ class GdprSpec extends PrivacyBaseSpec { privacy.resolvedPrivacy?.tcf?.tcfConsentVersion == 2 !privacy.resolvedPrivacy?.tcf?.inEea - privacy.privacyActionsPerBidder[BidderName.GENERIC].isEmpty() + privacy.privacyActionsPerBidder[GENERIC].isEmpty() privacy.errors == ["Amp request parameter consent_string or gdpr_consent have invalid format:" + " $invalidConsentString" as String] @@ -183,8 +123,8 @@ class GdprSpec extends PrivacyBaseSpec { def response = defaultPbsService.sendAmpRequest(ampRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.PREBID]*.code == [999] - assert response.ext?.errors[ErrorType.PREBID]*.message == + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == ["Amp request parameter gdpr_consent has invalid format for consent type tcfV2: $invalidTcfConsent" as String] where: @@ -211,8 +151,8 @@ class GdprSpec extends PrivacyBaseSpec { def response = defaultPbsService.sendAmpRequest(ampRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.PREBID]*.code == [999] - assert response.ext?.errors[ErrorType.PREBID]*.message == ["Consent type tcfV1 is no longer supported"] + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == ["Consent type tcfV1 is no longer supported"] } def "PBS should emit error for amp request with consentString when consent_type is bogus"() { @@ -235,8 +175,8 @@ class GdprSpec extends PrivacyBaseSpec { def response = defaultPbsService.sendAmpRequest(ampRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.PREBID]*.code == [999] - assert response.ext?.errors[ErrorType.PREBID]*.message == ["Invalid consent_type param passed"] + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == ["Invalid consent_type param passed"] } def "PBS should emit error for amp request when set not appropriate ccpa consent"() { @@ -257,8 +197,71 @@ class GdprSpec extends PrivacyBaseSpec { def response = defaultPbsService.sendAmpRequest(ampRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.PREBID]*.code == [999] - assert response.ext?.errors[ErrorType.PREBID]*.message == + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == ["Amp request parameter consent_string has invalid format for consent type tcfV2: $ccpaConsent" as String] } + + def "PBS should apply gdpr when privacy.gdpr.channel-enabled.amp or privacy.gdpr.enabled = true in account config"() { + given: "Default AmpRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def ampRequest = getGdprAmpRequest(validConsentString) + + and: "Save storedRequest into DB" + def ampStoredRequest = storedRequestWithGeo + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Save account config into DB" + def privacy = new AccountPrivacyConfig(gdpr: gdprConfig) + def accountConfig = new AccountConfig(privacy: privacy) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequests.device?.geo == maskGeo(ampStoredRequest) + + where: + gdprConfig << [new AccountGdprConfig(enabled: false, channelEnabled: [(ChannelType.AMP): true]), + new AccountGdprConfig(enabled: true)] + } + + def "PBS should not apply gdpr when privacy.gdpr.channel-enabled.amp or privacy.gdpr.enabled = false in account config"() { + given: "Default AmpRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def ampRequest = getGdprAmpRequest(validConsentString) + + and: "Save storedRequest into DB" + def ampStoredRequest = storedRequestWithGeo + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Save account config into DB" + def privacy = new AccountPrivacyConfig(gdpr: gdprConfig) + def accountConfig = new AccountConfig(privacy: privacy) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequests.device?.geo?.lat == ampStoredRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == ampStoredRequest.device.geo.lon + + where: + gdprConfig << [new AccountGdprConfig(enabled: true, channelEnabled: [(ChannelType.AMP): false]), + new AccountGdprConfig(enabled: false)] + } } 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 new file mode 100644 index 00000000000..7e04f7b4f49 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -0,0 +1,202 @@ +package org.prebid.server.functional.tests.privacy + +import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountGdprConfig +import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.testcontainers.PBSTest +import org.prebid.server.functional.util.privacy.BogusConsent +import org.prebid.server.functional.util.privacy.TcfConsent +import spock.lang.PendingFeature +import spock.lang.Unroll + +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.util.privacy.TcfConsent.GENERIC_VENDOR_ID +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS + +@PBSTest +class GdprAuctionSpec extends PrivacyBaseSpec { + + def setupSpec(){ + cacheVendorList() + } + + @PendingFeature + def "PBS should add debug log for auction request when valid gdpr was passed"() { + given: "Default gdpr BidRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + def bidRequest = getGdprBidRequest(validConsentString) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain debug log" + assert response.ext?.debug?.privacy + def privacy = response.ext?.debug?.privacy + + verifyAll { + privacy.originPrivacy?.tcf?.gdpr == "1" + privacy.originPrivacy?.tcf?.tcfConsentString == validConsentString as String + !privacy.originPrivacy?.tcf?.tcfConsentVersion + !privacy.originPrivacy?.tcf?.inEea + privacy.resolvedPrivacy?.tcf?.gdpr == "1" + privacy.resolvedPrivacy?.tcf?.tcfConsentString == validConsentString as String + privacy.resolvedPrivacy?.tcf?.tcfConsentVersion == 2 + !privacy.resolvedPrivacy?.tcf?.inEea + + !privacy.originPrivacy?.ccpa?.usPrivacy + !privacy.resolvedPrivacy?.ccpa?.usPrivacy + + !privacy.originPrivacy?.coppa?.coppa + privacy.resolvedPrivacy?.coppa?.coppa == 0 + + privacy.privacyActionsPerBidder[GENERIC] == + ["Geolocation was masked in request to bidder according to TCF policy."] + + privacy.errors?.isEmpty() + } + } + + @PendingFeature + def "PBS should add debug log for auction request when invalid gdpr was passed"() { + given: "Default gdpr BidRequest" + def invalidConsentString = new BogusConsent() + def bidRequest = getGdprBidRequest(invalidConsentString) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should not contain ext.errors" + assert !response.ext?.errors + + and: "Response should contain debug log with error" + assert response.ext?.debug?.privacy + def privacy = response.ext?.debug?.privacy + + verifyAll { + privacy.originPrivacy?.tcf?.gdpr == "1" + privacy.originPrivacy?.tcf?.tcfConsentString == invalidConsentString as String + !privacy.originPrivacy?.tcf?.tcfConsentVersion + !privacy.originPrivacy?.tcf?.inEea + privacy.resolvedPrivacy?.tcf?.gdpr == "1" + privacy.resolvedPrivacy?.tcf?.tcfConsentString == invalidConsentString as String + privacy.resolvedPrivacy?.tcf?.tcfConsentVersion == 2 + !privacy.resolvedPrivacy?.tcf?.inEea + + privacy.privacyActionsPerBidder[GENERIC].isEmpty() + + privacy.errors == ["Placeholder: invalid consent string"] + } + } + + @Unroll + def "PBS should apply gdpr when privacy.gdpr.channel-enabled.app or privacy.gdpr.enabled = true in account config"() { + given: "Default basic generic BidRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString) + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id, gdprConfig)) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo == maskGeo(bidRequest) + + where: + gdprConfig << [new AccountGdprConfig(enabled: false, channelEnabled: [(ChannelType.APP): true]), + new AccountGdprConfig(enabled: true)] + } + + @Unroll + def "PBS should apply gdpr when privacy.gdpr.channel-enabled.web or privacy.gdpr.enabled = true in account config"() { + given: "Default basic generic BidRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def bidRequest = getGdprBidRequest(validConsentString) + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.site.publisher.id, gdprConfig)) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo == maskGeo(bidRequest) + + where: + gdprConfig << [new AccountGdprConfig(enabled: true), + new AccountGdprConfig(enabled: false, channelEnabled: [(WEB): true])] + } + + @Unroll + def "PBS should not apply gdpr when privacy.gdpr.channel-enabled.app or privacy.gdpr.enabled = false in account config"() { + given: "Default basic generic BidRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString) + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id, gdprConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon + + where: + gdprConfig << [new AccountGdprConfig(enabled: true, channelEnabled: [(ChannelType.APP): false]), + new AccountGdprConfig(enabled: false)] + } + + @Unroll + def "PBS should not apply gdpr when privacy.gdpr.channel-enabled.web or privacy.gdpr.enabled = false in account config"() { + given: "Default basic generic BidRequest" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def bidRequest = getGdprBidRequest(validConsentString) + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.site.publisher.id, gdprConfig)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain not masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat + assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon + + where: + gdprConfig << [new AccountGdprConfig(enabled: true, channelEnabled: [(WEB): false]), + new AccountGdprConfig(enabled: false)] + } + + private Account getAccountWithGdpr(String accountId, AccountGdprConfig gdprConfig){ + def privacy = new AccountPrivacyConfig(gdpr: gdprConfig) + def accountConfig = new AccountConfig(privacy: privacy) + new Account(uuid: accountId, config: accountConfig) + } +} 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 7c010848da0..57ab9cbb318 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 @@ -3,31 +3,47 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.request.amp.AmpRequest 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.DistributionChannel import org.prebid.server.functional.model.request.auction.Geo import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.PBSTest import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.ConsentString +import org.prebid.server.functional.util.privacy.TcfConsent +import spock.lang.Shared import static org.prebid.server.functional.model.request.amp.ConsentType.TCF_2 import static org.prebid.server.functional.model.request.amp.ConsentType.US_PRIVACY +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS @PBSTest abstract class PrivacyBaseSpec extends BaseSpec { private static final int GEO_PRECISION = 2 + @Shared + protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService( + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]) - protected static BidRequest getBidRequestWithGeo() { - BidRequest.defaultBidRequest.tap { + protected static BidRequest getBidRequestWithGeo(DistributionChannel channel = SITE) { + BidRequest.getDefaultBidRequest(channel).tap { device = new Device(geo: new Geo(lat: PBSUtils.getFractionalRandomNumber(0, 90), lon: PBSUtils.getFractionalRandomNumber(0, 90))) } } - protected static BidRequest getCcpaBidRequest(ConsentString consentString) { - bidRequestWithGeo.tap { + protected static BidRequest getStoredRequestWithGeo() { + BidRequest.defaultStoredRequest.tap { + device = new Device(geo: new Geo(lat: PBSUtils.getFractionalRandomNumber(0, 90), lon: PBSUtils.getFractionalRandomNumber(0, 90))) + } + } + + protected static BidRequest getCcpaBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { + getBidRequestWithGeo(channel).tap { regs.ext = new RegsExt(usPrivacy: consentString) } } @@ -39,8 +55,8 @@ abstract class PrivacyBaseSpec extends BaseSpec { } } - protected static BidRequest getGdprBidRequest(ConsentString consentString) { - bidRequestWithGeo.tap { + protected static BidRequest getGdprBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { + getBidRequestWithGeo(channel).tap { regs.ext = new RegsExt(gdpr: 1) user = new User(ext: new UserExt(consent: consentString)) } @@ -51,6 +67,7 @@ abstract class PrivacyBaseSpec extends BaseSpec { gdprConsent = consentString consentType = TCF_2 gdprApplies = true + timeout = 5000 } } @@ -60,4 +77,19 @@ abstract class PrivacyBaseSpec extends BaseSpec { geo.lon = PBSUtils.getRoundedFractionalNumber(bidRequest.device.geo.lon, precision) geo } + + protected static void cacheVendorList(PrebidServerService pbsService = defaultPbsService) { + def isVendorListCachedClosure = { + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .addVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + def bidRequest = getGdprBidRequest(validConsentString) + + pbsService.sendAuctionRequest(bidRequest) + + pbsService.sendCollectedMetricsRequest()["privacy.tcf.v2.vendorlist.missing"] == 0 + } + PBSUtils.waitUntil(isVendorListCachedClosure, 10000, 1000) + } } 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 5eb0bc4bc51..1ee57da710c 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 @@ -9,6 +9,8 @@ class TcfConsent implements ConsentString { private static final String VENDOR_LIST_URL = "https://vendor-list.consensu.org/v2/vendor-list.json" private static final Integer VENDOR_LIST_VERSION = vendorListVersion + public static final Integer RUBICON_VENDOR_ID = 52 + public static final Integer GENERIC_VENDOR_ID = RUBICON_VENDOR_ID private final TCStringEncoder.Builder tcStringEncoder @@ -72,6 +74,11 @@ class TcfConsent implements ConsentString { this } + Builder addVendorLegitimateInterest(List vendorLegitimateInterest) { + tcStringEncoder.addVendorLegitimateInterest(BitSetIntIterable.from(vendorLegitimateInterest)) + this + } + Builder setPurposesLITransparency(PurposeId purposesLITransparency) { tcStringEncoder.addPurposesLITransparency(purposesLITransparency.value) this