diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java index ae139798dcf..dff0f808435 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java @@ -23,6 +23,7 @@ import org.prebid.server.settings.model.Purposes; import org.prebid.server.settings.model.activity.AccountActivityConfiguration; import org.prebid.server.settings.model.activity.privacy.AccountPrivacyModuleConfig; +import org.prebid.server.settings.model.activity.rule.AccountActivityRuleConfig; import java.util.Arrays; import java.util.Collections; @@ -171,7 +172,8 @@ private ActivityController from(Activity activity, final boolean allow = allowFromConfig(activityConfiguration.getAllow()); final List rules = ListUtils.emptyIfNull(activityConfiguration.getRules()).stream() .filter(Objects::nonNull) - .map(ruleConfiguration -> activityRuleFactory.from(ruleConfiguration, creationContext)) + .map(ruleConfiguration -> createRule(ruleConfiguration, creationContext)) + .filter(Objects::nonNull) .toList(); return ActivityController.of(allow, rules, debug); @@ -181,6 +183,20 @@ private static boolean allowFromConfig(Boolean configValue) { return configValue != null ? configValue : ActivityInfrastructure.ALLOW_ACTIVITY_BY_DEFAULT; } + private Rule createRule(AccountActivityRuleConfig ruleConfiguration, + ActivityControllerCreationContext creationContext) { + + try { + return activityRuleFactory.from(ruleConfiguration, creationContext); + } catch (Exception e) { + logger.error("ActivityInfrastructure rule creation failed: %s. Configuration: %s" + .formatted(e.getMessage(), ruleConfiguration)); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } + } + private static Supplier> enumMapFactory() { return () -> new EnumMap<>(Activity.class); } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java index 026e856a585..357eb916b5a 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java @@ -14,9 +14,10 @@ import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicDataSupplier; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicModule; import org.prebid.server.auction.gpp.model.GppContext; -import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.json.DecodeException; import org.prebid.server.json.JsonLogic; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.SettingsCache; @@ -34,6 +35,9 @@ public class USCustomLogicModuleCreator implements PrivacyModuleCreator { + private static final Logger logger = LoggerFactory.getLogger(USCustomLogicModuleCreator.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final Set ALLOWED_SECTIONS_IDS = PrivacySection.US_PRIVACY_SECTIONS.stream() .map(PrivacySection::sectionId) @@ -43,16 +47,19 @@ public class USCustomLogicModuleCreator implements PrivacyModuleCreator { private final JsonLogic jsonLogic; private final Map jsonLogicNodesCache; private final Metrics metrics; + private final double samplingRate; public USCustomLogicModuleCreator(USCustomLogicGppReaderFactory gppReaderFactory, JsonLogic jsonLogic, Integer cacheTtl, Integer cacheSize, - Metrics metrics) { + Metrics metrics, + double samplingRate) { this.gppReaderFactory = Objects.requireNonNull(gppReaderFactory); this.jsonLogic = Objects.requireNonNull(jsonLogic); this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; jsonLogicNodesCache = cacheTtl != null && cacheSize != null ? SettingsCache.createCache(cacheTtl, cacheSize, 0) @@ -76,6 +83,7 @@ public PrivacyModule from(PrivacyModuleCreationContext creationContext) { ? SetUtils.emptyIfNull(scope.getSectionsIds()).stream() .filter(sectionId -> shouldApplyPrivacy(sectionId, moduleConfig)) .map(sectionId -> forConfig(sectionId, normalizeSection, scope.getGppModel(), jsonLogicConfig)) + .filter(Objects::nonNull) .toList() : Collections.emptyList(); @@ -123,25 +131,25 @@ private PrivacyModule forConfig(int sectionId, GppModel gppModel, ObjectNode jsonLogicConfig) { - return new USCustomLogicModule( - jsonLogic, - jsonLogicNode(jsonLogicConfig), - USCustomLogicDataSupplier.of(gppReaderFactory.forSection(sectionId, normalizeSection, gppModel))); + try { + return new USCustomLogicModule( + jsonLogic, + jsonLogicNode(jsonLogicConfig), + USCustomLogicDataSupplier.of(gppReaderFactory.forSection(sectionId, normalizeSection, gppModel))); + } catch (Exception e) { + conditionalLogger.error( + "USCustomLogic creation failed: %s. Config: %s".formatted(e.getMessage(), jsonLogicConfig), + samplingRate); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } } private JsonLogicNode jsonLogicNode(ObjectNode jsonLogicConfig) { final String jsonAsString = jsonLogicConfig.toString(); return jsonLogicNodesCache != null - ? jsonLogicNodesCache.computeIfAbsent(jsonAsString, this::parseJsonLogicNode) - : parseJsonLogicNode(jsonAsString); - } - - private JsonLogicNode parseJsonLogicNode(String jsonLogicConfig) { - try { - return jsonLogic.parse(jsonLogicConfig); - } catch (DecodeException e) { - metrics.updateAlertsMetrics(MetricName.general); - throw new InvalidAccountConfigException("JsonLogic exception: " + e.getMessage()); - } + ? jsonLogicNodesCache.computeIfAbsent(jsonAsString, jsonLogic::parse) + : jsonLogic.parse(jsonAsString); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java index 817cbff36cc..f7d7f0fbfcd 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java @@ -11,6 +11,11 @@ import org.prebid.server.activity.infrastructure.privacy.PrivacySection; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatModule; import org.prebid.server.auction.gpp.model.GppContext; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import java.util.List; @@ -20,15 +25,22 @@ public class USNatModuleCreator implements PrivacyModuleCreator { + private static final Logger logger = LoggerFactory.getLogger(USNatModuleCreator.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final Set ALLOWED_SECTIONS_IDS = PrivacySection.US_PRIVACY_SECTIONS.stream() .map(PrivacySection::sectionId) .collect(Collectors.toSet()); private final USNatGppReaderFactory gppReaderFactory; + private final Metrics metrics; + private final double samplingRate; - public USNatModuleCreator(USNatGppReaderFactory gppReaderFactory) { + public USNatModuleCreator(USNatGppReaderFactory gppReaderFactory, Metrics metrics, double samplingRate) { this.gppReaderFactory = Objects.requireNonNull(gppReaderFactory); + this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; } @Override @@ -48,6 +60,7 @@ public PrivacyModule from(PrivacyModuleCreationContext creationContext) { sectionId, scope.getGppModel(), moduleConfig.getConfig())) + .filter(Objects::nonNull) .toList(); return new AndPrivacyModules(innerPrivacyModules); @@ -70,6 +83,16 @@ private PrivacyModule forSection(Activity activity, GppModel gppModel, AccountUSNatModuleConfig.Config config) { - return new USNatModule(activity, gppReaderFactory.forSection(sectionId, gppModel), config); + try { + return new USNatModule(activity, gppReaderFactory.forSection(sectionId, gppModel), config); + } catch (Exception e) { + conditionalLogger.error( + "UsNat privacy module creation failed: %s. Activity: %s. Section: %s. Gpp: %s.".formatted( + e.getMessage(), activity, sectionId, gppModel != null ? gppModel.encode() : null), + samplingRate); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java index cf25e716c5d..80c13e322d9 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java @@ -10,6 +10,10 @@ import org.prebid.server.activity.infrastructure.privacy.SkippedPrivacyModule; import org.prebid.server.activity.infrastructure.rule.AndRule; import org.prebid.server.activity.infrastructure.rule.Rule; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.activity.privacy.AccountPrivacyModuleConfig; import org.prebid.server.settings.model.activity.rule.AccountActivityPrivacyModulesRuleConfig; @@ -23,15 +27,19 @@ public class PrivacyModulesRuleCreator extends AbstractRuleCreator { + private static final Logger logger = LoggerFactory.getLogger(PrivacyModulesRuleCreator.class); + private static final String WILDCARD = "*"; private final Map privacyModulesCreators; + private final Metrics metrics; - public PrivacyModulesRuleCreator(List privacyModulesCreators) { + public PrivacyModulesRuleCreator(List privacyModulesCreators, Metrics metrics) { super(AccountActivityPrivacyModulesRuleConfig.class); this.privacyModulesCreators = Objects.requireNonNull(privacyModulesCreators).stream() .collect(Collectors.toMap(PrivacyModuleCreator::qualifier, UnaryOperator.identity())); + this.metrics = Objects.requireNonNull(metrics); } @Override @@ -44,8 +52,8 @@ protected Rule fromConfiguration(AccountActivityPrivacyModulesRuleConfig ruleCon .map(configuredModuleName -> mapToModulesQualifiers(configuredModuleName, creationContext)) .flatMap(Collection::stream) .filter(qualifier -> !creationContext.isUsed(qualifier)) - .peek(creationContext::use) .map(qualifier -> createPrivacyModule(qualifier, creationContext)) + .filter(Objects::nonNull) .toList(); return new AndRule(privacyModules); @@ -84,11 +92,22 @@ private PrivacyModule createPrivacyModule(PrivacyModuleQualifier privacyModuleQu ActivityControllerCreationContext creationContext) { if (creationContext.getSkipPrivacyModules().contains(privacyModuleQualifier)) { + creationContext.use(privacyModuleQualifier); return new SkippedPrivacyModule(privacyModuleQualifier); } - return privacyModulesCreators.get(privacyModuleQualifier) - .from(creationContext(privacyModuleQualifier, creationContext)); + try { + final PrivacyModule privacyModule = privacyModulesCreators.get(privacyModuleQualifier) + .from(creationContext(privacyModuleQualifier, creationContext)); + creationContext.use(privacyModuleQualifier); + + return privacyModule; + } catch (Exception e) { + logger.error("PrivacyModule %s creation failed: %s.".formatted(privacyModuleQualifier, e.getMessage())); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } } private static PrivacyModuleCreationContext creationContext(PrivacyModuleQualifier privacyModuleQualifier, diff --git a/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java b/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java index 486951c276f..d6561b8fe32 100644 --- a/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java @@ -35,8 +35,11 @@ USNatGppReaderFactory usNatGppReaderFactory() { } @Bean - USNatModuleCreator usNatModuleCreator(USNatGppReaderFactory gppReaderFactory) { - return new USNatModuleCreator(gppReaderFactory); + USNatModuleCreator usNatModuleCreator(USNatGppReaderFactory gppReaderFactory, + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new USNatModuleCreator(gppReaderFactory, metrics, logSamplingRate); } } @@ -54,9 +57,16 @@ USCustomLogicModuleCreator usCustomLogicModuleCreator( JsonLogic jsonLogic, @Value("${settings.in-memory-cache.ttl-seconds:#{null}}") Integer ttlSeconds, @Value("${settings.in-memory-cache.cache-size:#{null}}") Integer cacheSize, - Metrics metrics) { - - return new USCustomLogicModuleCreator(gppReaderFactory, jsonLogic, ttlSeconds, cacheSize, metrics); + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new USCustomLogicModuleCreator( + gppReaderFactory, + jsonLogic, + ttlSeconds, + cacheSize, + metrics, + logSamplingRate); } } } @@ -65,13 +75,15 @@ USCustomLogicModuleCreator usCustomLogicModuleCreator( static class RuleCreatorConfiguration { @Bean - ConditionsRuleCreator geoRuleCreator() { + ConditionsRuleCreator conditionsRuleCreator() { return new ConditionsRuleCreator(); } @Bean - PrivacyModulesRuleCreator privacyModulesRuleCreator(List privacyModuleCreators) { - return new PrivacyModulesRuleCreator(privacyModuleCreators); + PrivacyModulesRuleCreator privacyModulesRuleCreator(List privacyModuleCreators, + Metrics metrics) { + + return new PrivacyModulesRuleCreator(privacyModuleCreators, metrics); } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy index 2dd15697102..4f45bc9e512 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy @@ -30,7 +30,6 @@ import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData import java.time.Instant -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED @@ -825,8 +824,11 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Generic BidRequest with gpp and account setup" + def "PBS auction call when custom privacy regulation empty and normalize is disabled should process request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { @@ -860,14 +862,16 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes auction requests" activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Generic bidder should be called due to positive allow in activities" + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS auction call when custom privacy regulation with normalizing should ignore call to bidder"() { @@ -1571,8 +1575,11 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Store bid request with gpp string and link for account" + def "PBS amp call when custom privacy regulation empty and normalize is disabled should process request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def ampStoredRequest = BidRequest.defaultBidRequest.tap { @@ -1615,15 +1622,16 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp requests" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Generic bidder should be called" + assert bidder.getBidderRequests(ampStoredRequest.id) and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS amp call when custom privacy regulation with normalizing should ignore call to bidder"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy index c95acb92475..baac79520c3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy @@ -31,7 +31,6 @@ import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData import java.time.Instant -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED @@ -58,8 +57,8 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 @@ -482,6 +481,96 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS cookie sync call when privacy module contain invalid GPP segment should respond with required bidder URL and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Cookie sync request with link to account" + def accountId = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = INVALID_GPP_STRING + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain bidders userSync.urls" + assert response.getBidderUserSync(GENERIC).userSync.url + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Response shouldn't contain warnings" + assert !response.warnings + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: SYNC_USER. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS cookie sync call when privacy module contain invalid GPP string should respond with required bidder URL and emit warning in response"() { + given: "Cookie sync request with link to account" + def accountId = PBSUtils.randomString + def invalidGpp = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = invalidGpp + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain bidders userSync.urls" + assert response.getBidderUserSync(GENERIC).userSync.url + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + and: "Should add a warning when in debug mode" + assert response.warnings == ["GPP string invalid: Unable to decode '$invalidGpp'"] + } + def "PBS cookie sync call when request have different gpp consent but match and rejecting should exclude bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString @@ -579,14 +668,27 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes cookie sync request" def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) then: "Response should contain bidders userSync.urls" assert response.getBidderUserSync(GENERIC).userSync.url + and: "Response shouldn't contain warnings" + assert !response.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + where: - regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS cookie sync call when privacy regulation have duplicate should include proper responded with bidders URLs"() { @@ -742,7 +844,10 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { } def "PBS cookie sync call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Generic BidRequest with gpp and account setup" + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { @@ -773,17 +878,18 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should contain bidders userSync.urls" + assert response.getBidderUserSync(GENERIC).userSync.url and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, 'USCustomLogic creation failed: objects must have exactly 1 key defined, found 0').size() == 1 } def "PBS cookie sync when custom privacy regulation with normalizing should exclude bidders URLs"() { @@ -1325,6 +1431,97 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS setuid request when privacy module contain invalid GPP segment should respond with valid bidders UIDs cookies"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Cookie sync SetuidRequest with accountId" + def accountId = PBSUtils.randomString + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.gpp = INVALID_GPP_STRING + } + + and: "UIDS Cookie" + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain uids cookie" + assert response.uidsCookie + assert response.responseBody + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: SYNC_USER. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS setuid request when privacy module contain invalid GPP string should respond with valid bidders UIDs cookies"() { + given: "Cookie sync SetuidRequest with accountId" + def accountId = PBSUtils.randomString + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.gpp = PBSUtils.randomString + } + + and: "UIDS Cookie" + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain uids cookie" + assert response.uidsCookie + assert response.responseBody + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 + } + def "PBS setuid request when request have different gpp consent but match and rejecting should reject bidders with status code invalidStatusCode"() { given: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomString @@ -1433,6 +1630,9 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes cookie sync request" def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) @@ -1440,8 +1640,15 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert response.uidsCookie assert response.responseBody + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + where: - regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS setuid request when privacy regulation have duplicate should respond with valid bidders UIDs cookies"() { @@ -1611,8 +1818,11 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS setuid call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Cookie sync SetuidRequest with accountId" + def "PBS setuid call when custom privacy regulation empty and normalize is disabled should respond with required UIDs cookies"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomString def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { @@ -1646,17 +1856,18 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes setuid request" - activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should contain uids cookie" + assert response.responseBody and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, 'USCustomLogic creation failed: objects must have exactly 1 key defined, found 0').size() == 1 } def "PBS setuid call when custom privacy regulation with normalizing should reject bidders with status code invalidStatusCode"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index a825f10c287..3d595ab0612 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -29,7 +29,6 @@ import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData import java.time.Instant -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED @@ -56,10 +55,10 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT -import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 @@ -76,6 +75,7 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO @@ -192,7 +192,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes auction requests" activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 @@ -841,6 +841,105 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS auction call when privacy module contain invalid GPP segment shouldn't remove EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default Generic BidRequests with EIDS fields and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = INVALID_GPP_STRING + } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_EIDS. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS auction call when privacy module contain invalid GPP string shouldn't remove EIDS fields in request and emit warning in response"() { + given: "Default Generic BidRequests with EIDS fields and account id" + def accountId = PBSUtils.randomNumber as String + def invalidGpp = PBSUtils.randomString + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = invalidGpp + } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + } + def "PBS auction call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String @@ -938,15 +1037,32 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(bidRequest) + def response = activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + where: - regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS auction call when privacy regulation have duplicate should leave EIDS fields in request and update alerts metrics"() { @@ -1103,8 +1219,11 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Generic BidRequest with gpp and account setup" + def "PBS auction call when custom privacy regulation empty and normalize is disabled should leave EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { @@ -1135,16 +1254,25 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(bidRequest) + def response = activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS auction call when custom privacy regulation with normalizing that match custom config should have empty EIDS fields"() { @@ -1263,7 +1391,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1299,7 +1427,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1340,7 +1468,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1377,7 +1505,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1397,7 +1525,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 @@ -1415,7 +1543,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1449,7 +1577,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1488,7 +1616,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { it.regs.ext = new RegsExt(gpc: null) } - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1534,7 +1662,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1576,7 +1704,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -1619,7 +1747,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -1784,7 +1912,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -1829,12 +1957,127 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when privacy module contain invalid GPP segment shouldn't remove EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = INVALID_GPP_STRING + it.consentType = GPP + } + + and: "Activities set for transmitEIDS with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] + + "Response should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_EIDS. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS amp call when privacy module contain invalid GPP string shouldn't remove EIDS fields in request and emit warning in response"() { + given: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def invalidGpp = PBSUtils.randomString + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = invalidGpp + it.consentType = GPP + } + + and: "Activities set for transmitEIDS with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $invalidGpp"] + } + def "PBS amp call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = gppSid.value @@ -1885,7 +2128,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -1922,12 +2165,70 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when regs.gpp empty in request should leave EIDS fields in request"() { + given: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = regsGpp + it.consentType = GPP + } + + and: "Activities set for transmitEIDS with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + + where: + regsGpp << [null, ""] + } + def "PBS amp call when regs.gpp in request is allowing should leave EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -1954,14 +2255,20 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in EIDS fields" def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + assert genericBidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $regsGpp"] where: - regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS amp call when privacy regulation have duplicate should leave EIDS fields in request and update alerts metrics"() { @@ -1969,7 +2276,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2015,7 +2322,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2155,8 +2462,11 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Store bid request with link for account" + def "PBS amp call when custom privacy regulation empty and normalize is disabled should leave EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Store bid request with link for account" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) @@ -2195,17 +2505,25 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp requests" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string error" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $gppConsent"] and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy index 4707633d01d..5532caaf523 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy @@ -28,7 +28,6 @@ import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData import java.time.Instant -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED @@ -1528,8 +1527,11 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Generic BidRequest with gpp and account setup" + def "PBS auction call when custom privacy regulation empty and normalize is disabled should not round lat/lon data and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { @@ -1565,14 +1567,38 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes auction requests" activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Bidder request should contain not rounded geo data for device and user" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon + } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS auction call when custom privacy regulation with normalizing should change request consent and call to bidder"() { @@ -2810,8 +2836,11 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Store bid request with gpp string and link for account" + def "PBS amp call when custom privacy regulation empty and normalize is disabled should not round lat/lon data and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def ampStoredRequest = bidRequestWithGeo.tap { @@ -2854,15 +2883,38 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp requests" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Bidder request should contain not rounded geo data for device and user" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon + } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index 1dc12037a23..dab901bd93d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -15,16 +15,10 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities -import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition -import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device -import org.prebid.server.functional.model.request.auction.Eid 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.model.request.auction.UserExtData import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -39,7 +33,6 @@ import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData import java.time.Instant -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED @@ -66,10 +59,10 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT -import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 @@ -86,6 +79,7 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO @@ -251,7 +245,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes auction requests" activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 @@ -1113,6 +1107,143 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS auction call when privacy module contain invalid GPP segment shouldn't remove UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default Generic BidRequests with UFPD fields and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = INVALID_GPP_STRING + } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo.zip == bidRequest.user.geo.zip + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_UFPD. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS auction call when privacy module contain invalid GPP string shouldn't remove UFPD fields in request and emit warning in response"() { + given: "Default Generic BidRequests with UFPD fields and account id" + def accountId = PBSUtils.randomNumber as String + def invalidGpp = PBSUtils.randomString + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = invalidGpp + } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo.zip == bidRequest.user.geo.zip + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + } + def "PBS auction call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String @@ -1245,8 +1376,11 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(bidRequest) + def response = activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -1272,8 +1406,22 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Generic bidder request should have data in EIDS fields" assert bidderRequest.user.eids == bidRequest.user.eids + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics for disallowed activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + where: - regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS auction call when privacy regulation have duplicate should leave UFPD fields in request and update alerts metrics"() { @@ -1483,8 +1631,11 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Generic BidRequest with gpp and account setup" + def "PBS auction call when custom privacy regulation empty and normalize is disabled should leave UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { @@ -1515,16 +1666,45 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(bidRequest) + def response = activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + and: "Generic bidder should be called due to positive allow in activities" + verifyAll { + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS auction call when custom privacy regulation with normalizing that match custom config should have empty UFPD fields"() { @@ -1658,7 +1838,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1713,7 +1893,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1769,7 +1949,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1821,7 +2001,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1841,7 +2021,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 @@ -1859,7 +2039,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1912,7 +2092,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1966,7 +2146,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { it.regs.ext = new RegsExt(gpc: null) } - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -2027,7 +2207,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -2088,7 +2268,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2146,7 +2326,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2326,7 +2506,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2386,12 +2566,165 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when privacy module contain invalid GPP segment shouldn't remove UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default Generic BidRequest with UFPD fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = INVALID_GPP_STRING + it.consentType = GPP + } + + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + verifyAll { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] + + "Response should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_UFPD. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS amp call when privacy module contain invalid GPP string shouldn't remove UFPD fields in request and emit warning in response"() { + given: "Default Generic BidRequest with UFPD fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def invalidGpp = PBSUtils.randomString + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = invalidGpp + it.consentType = GPP + } + + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + verifyAll { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $invalidGpp"] + } + def "PBS amp call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = gppSid.value @@ -2457,7 +2790,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2513,12 +2846,89 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when regs.gpp empty in request should leave UFPD fields in request"() { + given: "Default Generic BidRequest with UFPD fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = regsGpp + it.consentType = GPP + } + + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + verifyAll { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + + where: + regsGpp << [null, ""] + } + def "PBS amp call when regs.gpp in request is allowing should leave UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2545,7 +2955,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) @@ -2570,8 +2980,14 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Generic bidder request should have data in EIDS fields" assert bidderRequest.user.eids == ampStoredRequest.user.eids + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $regsGpp"] + where: - regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS amp call when privacy regulation have duplicate should leave UFPD fields in request and update alerts metrics"() { @@ -2579,7 +2995,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2644,7 +3060,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = US_NAT_V1.value @@ -2817,8 +3233,11 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } - def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { - given: "Store bid request with link for account" + def "PBS amp call when custom privacy regulation empty and normalize is disabled should leave UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Store bid request with link for account" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId) @@ -2857,17 +3276,43 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp requests" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string error" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $gppConsent"] and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { 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 ed633ec5316..e16dc69dd62 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 @@ -95,6 +95,8 @@ abstract class PrivacyBaseSpec extends BaseSpec { private static final Map GDPR_EEA_COUNTRY = ["gdpr.eea-countries": "$BULGARIA.ISOAlpha2, SK, VK" as String] protected static final String VENDOR_LIST_PATH = "/app/prebid-server/data/vendorlist-v{VendorVersion}/{VendorVersion}.json" + protected static final String INVALID_GPP_SEGMENT = PBSUtils.getRandomString(7) + protected static final String INVALID_GPP_STRING = "DBABLA~${INVALID_GPP_SEGMENT}.YA" protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java index bdfa42035da..d0752c1223b 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java @@ -209,4 +209,39 @@ public void parseShouldReturnImitatedTransmitEidsActivity() { assertThat(controllers.get(Activity.TRANSMIT_UFPD).isAllowed(null)).isEqualTo(false); assertThat(controllers.get(Activity.TRANSMIT_EIDS).isAllowed(null)).isEqualTo(false); } + + @Test + public void parseShouldSkipRuleThatFailedCreation() { + // given + final Account account = Account.builder() + .privacy(AccountPrivacyConfig.builder() + .activities(Map.of( + Activity.SYNC_USER, AccountActivityConfiguration.of(null, null), + Activity.CALL_BIDDER, AccountActivityConfiguration.of(false, null), + Activity.MODIFY_UFDP, AccountActivityConfiguration.of(true, null), + Activity.TRANSMIT_UFPD, AccountActivityConfiguration.of(true, singletonList( + AccountActivityConditionsRuleConfig.of(null, null))))) + .build()) + .build(); + final GppContext gppContext = GppContextCreator.from(null, null).build().getGppContext(); + + given(activityRuleFactory.from( + same(account.getPrivacy().getActivities().get(Activity.TRANSMIT_UFPD).getRules().getFirst()), + argThat(arg -> arg.getGppContext() == gppContext))) + .willThrow(new IllegalArgumentException()); + + // when + final Map controllers = creator.parse(account, gppContext, debug); + + // then + assertThat(controllers.keySet()).containsExactlyInAnyOrder(Activity.values()); + + assertThat(controllers.get(Activity.SYNC_USER).isAllowed(null)) + .isEqualTo(ActivityInfrastructure.ALLOW_ACTIVITY_BY_DEFAULT); + assertThat(controllers.get(Activity.CALL_BIDDER).isAllowed(null)).isEqualTo(false); + assertThat(controllers.get(Activity.MODIFY_UFDP).isAllowed(null)).isEqualTo(true); + assertThat(controllers.get(Activity.TRANSMIT_UFPD).isAllowed(null)).isEqualTo(true); + + verify(metrics).updateAlertsMetrics(eq(MetricName.general)); + } } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java index 3d10dd5b9a4..cfba2457009 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java @@ -16,7 +16,6 @@ import org.prebid.server.activity.infrastructure.privacy.usnat.reader.USNationalGppReader; import org.prebid.server.activity.infrastructure.rule.Rule; import org.prebid.server.auction.gpp.model.GppContextCreator; -import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JsonLogic; import org.prebid.server.metric.MetricName; @@ -31,7 +30,6 @@ import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -63,7 +61,7 @@ public void setUp() { .willReturn(new USNationalGppReader(null)); given(jsonLogic.parse(any())).willReturn(JsonLogicBoolean.TRUE); - target = new USCustomLogicModuleCreator(gppReaderFactory, jsonLogic, null, null, metrics); + target = new USCustomLogicModuleCreator(gppReaderFactory, jsonLogic, null, null, metrics, 0); } @Test @@ -232,8 +230,11 @@ public void fromShouldThrowExceptionAndEmitMetricsOnInvalidJsonLogicConfig() { singletonList(7), givenConfig(singleton(7), null, Activity.CALL_BIDDER, mapper.createObjectNode())); - // when and then - assertThatExceptionOfType(InvalidAccountConfigException.class).isThrownBy(() -> target.from(creationContext)); + // when + final PrivacyModule privacyModule = target.from(creationContext); + + // then + assertThat(privacyModule.proceed(null)).isEqualTo(Rule.Result.ABSTAIN); verify(jsonLogic).parse(any()); verify(metrics).updateAlertsMetrics(eq(MetricName.general)); diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java index f1b098c6562..1125110bdc9 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java @@ -12,6 +12,8 @@ import org.prebid.server.activity.infrastructure.privacy.usnat.reader.USNationalGppReader; import org.prebid.server.activity.infrastructure.rule.Rule; import org.prebid.server.auction.gpp.model.GppContextCreator; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import java.util.List; @@ -35,13 +37,16 @@ public class USNatModuleCreatorTest { @Mock(strictness = LENIENT) private USNatGppReaderFactory gppReaderFactory; + @Mock(strictness = LENIENT) + private Metrics metrics; + private USNatModuleCreator target; @BeforeEach public void setUp() { given(gppReaderFactory.forSection(anyInt(), any())).willReturn(new USNationalGppReader(null)); - target = new USNatModuleCreator(gppReaderFactory); + target = new USNatModuleCreator(gppReaderFactory, metrics, 0); } @Test @@ -121,11 +126,36 @@ public void fromShouldShouldSkipConfiguredSectionsIds() { verifyNoMoreInteractions(gppReaderFactory); } + @Test + public void fromShouldShouldSkipSectionsWithInvalidGppSubstring() { + // given + given(gppReaderFactory.forSection(eq(7), any())) + .willReturn(new USNationalGppReader(null) { + + @Override + public Integer getMspaServiceProviderMode() { + throw new IllegalStateException(); + } + }); + + final PrivacyModuleCreationContext creationContext = givenCreationContext(singletonList(7), emptyList()); + + // when + target.from(creationContext); + + // then + verify(gppReaderFactory).forSection(eq(7), any()); + verify(metrics).updateAlertsMetrics(eq(MetricName.general)); + + verifyNoMoreInteractions(gppReaderFactory); + verifyNoMoreInteractions(metrics); + } + private static PrivacyModuleCreationContext givenCreationContext(List sectionsIds, List skipSectionsIds) { return PrivacyModuleCreationContext.of( - Activity.CALL_BIDDER, + Activity.TRANSMIT_UFPD, AccountUSNatModuleConfig.of(true, 0, AccountUSNatModuleConfig.Config.of(skipSectionsIds, false)), GppContextCreator.from(null, sectionsIds).build().getGppContext()); } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java index 095c52ed14e..1ce7438bc5e 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java @@ -11,6 +11,7 @@ import org.prebid.server.activity.infrastructure.privacy.PrivacyModuleQualifier; import org.prebid.server.activity.infrastructure.privacy.TestPrivacyModule; import org.prebid.server.activity.infrastructure.rule.Rule; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.activity.privacy.AccountPrivacyModuleConfig; import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import org.prebid.server.settings.model.activity.rule.AccountActivityPrivacyModulesRuleConfig; @@ -24,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; @ExtendWith(MockitoExtension.class) public class PrivacyModulesRuleCreatorTest { @@ -31,13 +33,16 @@ public class PrivacyModulesRuleCreatorTest { @Mock private PrivacyModuleCreator privacyModuleCreator; + @Mock(strictness = LENIENT) + private Metrics metrics; + private PrivacyModulesRuleCreator target; @BeforeEach public void setUp() { given(privacyModuleCreator.qualifier()).willReturn(PrivacyModuleQualifier.US_NAT); - target = new PrivacyModulesRuleCreator(singletonList(privacyModuleCreator)); + target = new PrivacyModulesRuleCreator(singletonList(privacyModuleCreator), metrics); } @Test @@ -177,7 +182,7 @@ public void fromShouldDisableSkippedPrivacyModule() { @Test public void fromShouldSkipPrivacyModuleWithoutCreator() { // given - target = new PrivacyModulesRuleCreator(emptyList()); + target = new PrivacyModulesRuleCreator(emptyList(), metrics); final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); @@ -191,6 +196,26 @@ public void fromShouldSkipPrivacyModuleWithoutCreator() { assertThat(rule.proceed(null)).isEqualTo(Rule.Result.ABSTAIN); } + @Test + public void fromShouldSkipPrivacyModuleThatFailedCreation() { + // given + final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( + singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); + final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, 0, null); + final ActivityControllerCreationContext creationContext = creationContext( + Map.of(PrivacyModuleQualifier.US_NAT, moduleConfig)); + + given(privacyModuleCreator.from(eq(PrivacyModuleCreationContext.of(null, moduleConfig, null)))) + .willThrow(new IllegalArgumentException()); + + // when + final Rule rule = target.from(config, creationContext); + + // then + assertThat(rule.proceed(null)).isEqualTo(Rule.Result.ABSTAIN); + assertThat(creationContext.isUsed(PrivacyModuleQualifier.US_NAT)).isFalse(); + } + private static ActivityControllerCreationContext creationContext( Map modulesConfigs) {