diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index e8b835f7400..5b498edfd40 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -70,6 +70,11 @@ live-intent-omni-channel-identity ${project.version} + + org.prebid.server.hooks.modules + pb-rule-engine + ${project.version} + diff --git a/extra/modules/pb-rule-engine/pom.xml b/extra/modules/pb-rule-engine/pom.xml new file mode 100644 index 00000000000..1903ee73045 --- /dev/null +++ b/extra/modules/pb-rule-engine/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.33.0-SNAPSHOT + + + pb-rule-engine + + pb-rule-engine + Rule engine module + diff --git a/extra/modules/pb-rule-engine/src/lombok.config b/extra/modules/pb-rule-engine/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java new file mode 100644 index 00000000000..2caf6ee8b30 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.rule.engine.config; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Vertx; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.modules.rule.engine.core.config.AccountConfigParser; +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.modules.rule.engine.core.config.StageConfigParser; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestStageSpecification; +import org.prebid.server.hooks.modules.rule.engine.v1.PbRuleEngineModule; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Clock; +import java.util.concurrent.ThreadLocalRandom; +import java.util.random.RandomGenerator; + +@Configuration +@ConditionalOnProperty(prefix = "hooks." + PbRuleEngineModule.CODE, name = "enabled", havingValue = "true") +public class PbRuleEngineModuleConfiguration { + + @Bean + PbRuleEngineModule ruleEngineModule(RuleParser ruleParser, + @Value("${datacenter-region:#{null}}") String datacenter) { + + return new PbRuleEngineModule(ruleParser, datacenter); + } + + @Bean + StageConfigParser processedAuctionRequestStageParser( + BidderCatalog bidderCatalog) { + + final RandomGenerator randomGenerator = () -> ThreadLocalRandom.current().nextLong(); + + return new StageConfigParser<>( + randomGenerator, + Stage.processed_auction_request, + new RequestStageSpecification(ObjectMapperProvider.mapper(), bidderCatalog, randomGenerator), + new RequestConditionalRuleFactory()); + } + + @Bean + AccountConfigParser accountConfigParser( + StageConfigParser processedAuctionRequestStageParser) { + + return new AccountConfigParser(ObjectMapperProvider.mapper(), processedAuctionRequestStageParser); + } + + @Bean + RuleParser ruleParser( + @Value("${hooks.pb-rule-engine.rule-cache.expire-after-minutes}") long cacheExpireAfterMinutes, + @Value("${hooks.pb-rule-engine.rule-cache.max-size}") long cacheMaxSize, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-initial-delay-millis}") long delay, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-max-delay-millis}") long maxDelay, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-factor}") double factor, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-jitter}") double jitter, + AccountConfigParser accountConfigParser, + Vertx vertx, + Clock clock) { + + return new RuleParser( + cacheExpireAfterMinutes, + cacheMaxSize, + ExponentialBackoffRetryPolicy.of(delay, maxDelay, factor, jitter), + accountConfigParser, + vertx, + clock); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java new file mode 100644 index 00000000000..6f542881c66 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; + +import java.util.Objects; + +public class AccountConfigParser { + + private final ObjectMapper mapper; + private final StageConfigParser processedAuctionRequestStageParser; + + public AccountConfigParser( + ObjectMapper mapper, + StageConfigParser processedAuctionRequestStageParser) { + + this.mapper = Objects.requireNonNull(mapper); + this.processedAuctionRequestStageParser = Objects.requireNonNull(processedAuctionRequestStageParser); + } + + public PerStageRule parse(ObjectNode accountConfig) { + final AccountConfig parsedConfig; + try { + parsedConfig = mapper.treeToValue(accountConfig, AccountConfig.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + + if (!parsedConfig.isEnabled()) { + return PerStageRule.builder() + .timestamp(parsedConfig.getTimestamp()) + .processedAuctionRequestRule(NoOpRule.create()) + .build(); + } + + return PerStageRule.builder() + .timestamp(parsedConfig.getTimestamp()) + .processedAuctionRequestRule(processedAuctionRequestStageParser.parse(parsedConfig)) + .build(); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java new file mode 100644 index 00000000000..3136a0cefca --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java @@ -0,0 +1,152 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.retry.Retryable; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public class RuleParser { + + private static final Logger logger = LoggerFactory.getLogger(RuleParser.class); + + private final AccountConfigParser parser; + private final Vertx vertx; + private final Clock clock; + + private final RetryPolicy retryPolicy; + + private final Map accountIdToParsingAttempt; + private final Map accountIdToRules; + + public RuleParser(long cacheExpireAfterMinutes, + long cacheMaxSize, + RetryPolicy retryPolicy, + AccountConfigParser parser, + Vertx vertx, + Clock clock) { + + this.parser = Objects.requireNonNull(parser); + this.vertx = Objects.requireNonNull(vertx); + this.clock = Objects.requireNonNull(clock); + this.retryPolicy = Objects.requireNonNull(retryPolicy); + + this.accountIdToParsingAttempt = Caffeine.newBuilder() + .expireAfterAccess(cacheExpireAfterMinutes, TimeUnit.MINUTES) + .maximumSize(cacheMaxSize) + .build() + .asMap(); + + this.accountIdToRules = Caffeine.newBuilder() + .expireAfterAccess(cacheExpireAfterMinutes, TimeUnit.MINUTES) + .maximumSize(cacheMaxSize) + .build() + .asMap(); + } + + public Future parseForAccount(String accountId, ObjectNode config) { + final PerStageRule cachedRule = accountIdToRules.get(accountId); + + if (cachedRule != null && cachedRule.timestamp().compareTo(getConfigTimestamp(config)) >= 0) { + return Future.succeededFuture(cachedRule); + } + + parseConfig(accountId, config); + return Future.succeededFuture(ObjectUtils.defaultIfNull(cachedRule, PerStageRule.noOp())); + } + + private Instant getConfigTimestamp(ObjectNode config) { + try { + return Optional.of(config) + .map(node -> node.get("timestamp")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText) + .map(Instant::parse) + .orElse(Instant.EPOCH); + } catch (DateTimeParseException exception) { + return Instant.EPOCH; + } + } + + private void parseConfig(String accountId, ObjectNode config) { + final Instant now = clock.instant(); + final ParsingAttempt attempt = accountIdToParsingAttempt.compute( + accountId, (ignored, previousAttempt) -> tryRegisteringNewAttempt(previousAttempt, now)); + + // reference equality used on purpose - if references are equal - then we should parse + if (attempt.timestamp() == now) { + logger.info("Parsing rule for account {}", accountId); + vertx.executeBlocking(() -> parser.parse(config)) + .onSuccess(result -> succeedParsingAttempt(accountId, result)) + .onFailure(error -> failParsingAttempt(accountId, attempt, error)); + } + } + + private ParsingAttempt tryRegisteringNewAttempt(ParsingAttempt previousAttempt, Instant currentAttemptStart) { + if (previousAttempt == null) { + return new ParsingAttempt.InProgress(currentAttemptStart, retryPolicy); + } + + if (previousAttempt instanceof ParsingAttempt.InProgress) { + return previousAttempt; + } + + if (previousAttempt.retryPolicy() instanceof Retryable previousAttemptRetryPolicy) { + final Instant previouslyDecidedToRetryAfter = previousAttempt.timestamp().plus( + Duration.ofMillis(previousAttemptRetryPolicy.delay())); + + return previouslyDecidedToRetryAfter.isBefore(currentAttemptStart) + ? new ParsingAttempt.InProgress(currentAttemptStart, previousAttemptRetryPolicy.next()) + : previousAttempt; + } + + return previousAttempt; + } + + private void succeedParsingAttempt(String accountId, PerStageRule result) { + accountIdToRules.put(accountId, result); + accountIdToParsingAttempt.remove(accountId); + + logger.debug("Successfully parsed rule-engine config for account {}", accountId); + } + + private void failParsingAttempt(String accountId, ParsingAttempt attempt, Throwable cause) { + accountIdToParsingAttempt.put(accountId, ((ParsingAttempt.InProgress) attempt).failed()); + + logger.error( + "Failed to parse rule-engine config for account %s: %s".formatted(accountId, cause.getMessage()), + cause); + } + + private sealed interface ParsingAttempt { + + Instant timestamp(); + + RetryPolicy retryPolicy(); + + record Failed(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt { + } + + record InProgress(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt { + + public Failed failed() { + return new Failed(timestamp, retryPolicy); + } + } + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java new file mode 100644 index 00000000000..12c2d9a2720 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java @@ -0,0 +1,175 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountRuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ModelGroupConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ResultFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.RuleSetConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.SchemaFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.AlternativeActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.CompositeRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.rules.DefaultActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RandomWeightedRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidMatcherConfiguration; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTreeFactory; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedEntry; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; +import java.util.random.RandomGenerator; + +public class StageConfigParser { + + private final RandomGenerator randomGenerator; + private final StageSpecification specification; + private final Stage stage; + private final ConditionalRuleFactory conditionalRuleFactory; + + public StageConfigParser(RandomGenerator randomGenerator, + Stage stage, + StageSpecification specification, + ConditionalRuleFactory conditionalRuleFactory) { + + this.randomGenerator = Objects.requireNonNull(randomGenerator); + this.stage = Objects.requireNonNull(stage); + this.specification = Objects.requireNonNull(specification); + this.conditionalRuleFactory = Objects.requireNonNull(conditionalRuleFactory); + } + + public Rule parse(AccountConfig config) { + final List> stageSubrules = config.getRuleSets().stream() + .filter(ruleSet -> stage.equals(ruleSet.getStage())) + .filter(RuleSetConfig::isEnabled) + .map(RuleSetConfig::getModelGroups) + .map(this::parseModelGroupConfigs) + .toList(); + + return stageSubrules.isEmpty() + ? NoOpRule.create() + : CompositeRule.of(stageSubrules); + } + + private Rule parseModelGroupConfigs(List modelGroupConfigs) { + final List>> weightedRules = modelGroupConfigs.stream() + .map(config -> WeightedEntry.of(config.getWeight(), parseModelGroupConfig(config))) + .toList(); + + return RandomWeightedRule.of(randomGenerator, new WeightedList<>(weightedRules)); + } + + private Rule parseModelGroupConfig(ModelGroupConfig config) { + final Rule matchingRule = parseMatchingRule(config); + final Rule defaultRule = parseDefaultActionRule(config); + + return combineRules(matchingRule, defaultRule); + } + + private Rule parseMatchingRule(ModelGroupConfig config) { + final List schemaConfig = config.getSchema(); + final List rulesConfig = config.getRules(); + + if (CollectionUtils.isEmpty(schemaConfig) || CollectionUtils.isEmpty(rulesConfig)) { + return null; + } + + final Schema schema = parseSchema(schemaConfig); + + final List> rules = rulesConfig.stream() + .map(this::parseRuleConfig) + .toList(); + final RuleTree> ruleTree = RuleTreeFactory.buildTree(rules); + + if (schemaConfig.size() != ruleTree.getDepth()) { + throw new InvalidMatcherConfiguration("Schema functions count and rules matchers count mismatch"); + } + + return conditionalRuleFactory.create(schema, ruleTree, config.getAnalyticsKey(), config.getVersion()); + } + + private Schema parseSchema(List schema) { + final List> schemaFunctions = schema.stream() + .map(config -> SchemaFunctionHolder.of( + config.getFunction(), + specification.schemaFunctionByName(config.getFunction()), + config.getArgs())) + .toList(); + + schemaFunctions.forEach(this::validateFunctionConfig); + + return Schema.of(schemaFunctions); + } + + private void validateFunctionConfig(SchemaFunctionHolder holder) { + try { + holder.getSchemaFunction().validateConfig(holder.getConfig()); + } catch (ConfigurationValidationException exception) { + throw new InvalidMatcherConfiguration( + "Function '%s' configuration is invalid: %s".formatted(holder.getName(), exception.getMessage())); + } + } + + private RuleConfig parseRuleConfig(AccountRuleConfig ruleConfig) { + final String ruleFired = String.join("|", ruleConfig.getConditions()); + final List> actions = parseActions(ruleConfig.getResults()); + + return RuleConfig.of(ruleFired, actions); + } + + private Rule parseDefaultActionRule(ModelGroupConfig config) { + final List defaultActionConfig = config.getDefaultAction(); + + if (CollectionUtils.isEmpty(config.getDefaultAction())) { + return null; + } + + return new DefaultActionRule<>( + parseActions(defaultActionConfig), config.getAnalyticsKey(), config.getVersion()); + } + + private List> parseActions(List functionConfigs) { + final List> actions = functionConfigs.stream() + .map(config -> ResultFunctionHolder.of( + config.getFunction(), + specification.resultFunctionByName(config.getFunction()), + config.getArgs())) + .toList(); + + actions.forEach(this::validateActionConfig); + + return actions; + } + + private void validateActionConfig(ResultFunctionHolder action) { + try { + action.getFunction().validateConfig(action.getConfig()); + } catch (ConfigurationValidationException exception) { + throw new InvalidMatcherConfiguration( + "Function '%s' configuration is invalid: %s".formatted(action.getName(), exception.getMessage())); + } + } + + private Rule combineRules(Rule left, Rule right) { + if (left == null && right == null) { + return NoOpRule.create(); + } else if (left != null && right != null) { + return AlternativeActionRule.of(left, right); + } else if (left != null) { + return AlternativeActionRule.of(left, NoOpRule.create()); + } + + return AlternativeActionRule.of(right, NoOpRule.create()); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java new file mode 100644 index 00000000000..fab0cd9f208 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +@Value +@Builder +@Jacksonized +public class AccountConfig { + + @Builder.Default + boolean enabled = true; + + @Builder.Default + Instant timestamp = Instant.EPOCH; + + @Builder.Default + @JsonProperty("ruleSets") + List ruleSets = Collections.emptyList(); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java new file mode 100644 index 00000000000..6feb59c8e66 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AccountRuleConfig { + + List conditions; + + List results; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java new file mode 100644 index 00000000000..f646de8985e --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +public class ModelGroupConfig { + + int weight; + + @JsonProperty("analyticsKey") + String analyticsKey; + + String version; + + List schema; + + @JsonProperty("default") + List defaultAction; + + List rules; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java new file mode 100644 index 00000000000..2b642c633e2 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ResultFunctionConfig { + + String function; + + ObjectNode args; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java new file mode 100644 index 00000000000..9c54f100977 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import org.prebid.server.hooks.execution.model.Stage; + +import java.util.List; + +@Value +@Builder +@Jacksonized +public class RuleSetConfig { + + @Builder.Default + boolean enabled = true; + + Stage stage; + + String name; + + String version; + + @JsonProperty("modelGroups") + List modelGroups; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java new file mode 100644 index 00000000000..264abef359a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SchemaFunctionConfig { + + String function; + + ObjectNode args; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java new file mode 100644 index 00000000000..e820c787a71 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +public sealed interface Granularity { + + final class Request implements Granularity { + private static final Request INSTANCE = new Request(); + + private Request() { + } + + public static Request instance() { + return INSTANCE; + } + } + + record Imp(String impId) implements Granularity { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java new file mode 100644 index 00000000000..5b644c8cde5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java @@ -0,0 +1,39 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; + +import java.util.Objects; + +public class PerImpConditionalRule implements Rule { + + private final ConditionalRule delegate; + + public PerImpConditionalRule(ConditionalRule delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + @Override + public RuleResult process(BidRequest value, RequestRuleContext context) { + RuleResult result = RuleResult.noAction(value); + for (Imp imp : value.getImp()) { + result = result.mergeWith(delegate.process(result.getValue(), contextForImp(context, imp))); + + if (result.isReject()) { + return result; + } + } + + return result; + } + + private RequestRuleContext contextForImp(RequestRuleContext context, Imp imp) { + return RequestRuleContext.of( + context.getAuctionContext(), + new Granularity.Imp(imp.getId()), + context.getDatacenter()); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java new file mode 100644 index 00000000000..7958d1e97b9 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +public class RequestConditionalRuleFactory implements ConditionalRuleFactory { + + @Override + public Rule create( + Schema schema, + RuleTree> ruleTree, + String analyticsKey, + String modelVersion) { + + final ConditionalRule requestMatchingRule = new ConditionalRule<>( + schema, ruleTree, analyticsKey, modelVersion); + + return schema.getFunctions().stream() + .map(SchemaFunctionHolder::getName) + .anyMatch(RequestStageSpecification.PER_IMP_SCHEMA_FUNCTIONS::contains) + + ? new PerImpConditionalRule(requestMatchingRule) + : requestMatchingRule; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java new file mode 100644 index 00000000000..35ea4e1f6b0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import lombok.Value; +import org.prebid.server.auction.model.AuctionContext; + +@Value(staticConstructor = "of") +public class RequestRuleContext { + + AuctionContext auctionContext; + + Granularity granularity; + + String datacenter; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java new file mode 100644 index 00000000000..953dd11a74b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java @@ -0,0 +1,103 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter.ExcludeBiddersFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter.IncludeBiddersFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log.LogATagFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.AdUnitCodeFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.AdUnitCodeInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.BundleFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.BundleInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.ChannelFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DataCenterFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DataCenterInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceCountryFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceCountryInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceTypeFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceTypeInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DomainFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DomainInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.EidAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.EidInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.FpdAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.GppSidAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.GppSidInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.MediaTypeInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.PrebidKeyFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.TcfInScopeFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.UserFpdAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidResultFunctionException; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidSchemaFunctionException; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.functions.PercentFunction; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.random.RandomGenerator; + +public class RequestStageSpecification implements StageSpecification { + + public static final Set PER_IMP_SCHEMA_FUNCTIONS = + Set.of(AdUnitCodeFunction.NAME, AdUnitCodeInFunction.NAME, MediaTypeInFunction.NAME); + + private final Map> schemaFunctions; + private final Map> resultFunctions; + + public RequestStageSpecification(ObjectMapper mapper, + BidderCatalog bidderCatalog, + RandomGenerator random) { + + schemaFunctions = new HashMap<>(); + schemaFunctions.put(AdUnitCodeFunction.NAME, new AdUnitCodeFunction()); + schemaFunctions.put(AdUnitCodeInFunction.NAME, new AdUnitCodeInFunction()); + schemaFunctions.put(BundleFunction.NAME, new BundleFunction()); + schemaFunctions.put(BundleInFunction.NAME, new BundleInFunction()); + schemaFunctions.put(ChannelFunction.NAME, new ChannelFunction()); + schemaFunctions.put(DataCenterFunction.NAME, new DataCenterFunction()); + schemaFunctions.put(DataCenterInFunction.NAME, new DataCenterInFunction()); + schemaFunctions.put(DeviceCountryFunction.NAME, new DeviceCountryFunction()); + schemaFunctions.put(DeviceCountryInFunction.NAME, new DeviceCountryInFunction()); + schemaFunctions.put(DeviceTypeFunction.NAME, new DeviceTypeFunction()); + schemaFunctions.put(DeviceTypeInFunction.NAME, new DeviceTypeInFunction()); + schemaFunctions.put(DomainFunction.NAME, new DomainFunction()); + schemaFunctions.put(DomainInFunction.NAME, new DomainInFunction()); + schemaFunctions.put(EidAvailableFunction.NAME, new EidAvailableFunction()); + schemaFunctions.put(EidInFunction.NAME, new EidInFunction()); + schemaFunctions.put(FpdAvailableFunction.NAME, new FpdAvailableFunction()); + schemaFunctions.put(GppSidAvailableFunction.NAME, new GppSidAvailableFunction()); + schemaFunctions.put(GppSidInFunction.NAME, new GppSidInFunction()); + schemaFunctions.put(MediaTypeInFunction.NAME, new MediaTypeInFunction()); + schemaFunctions.put(PercentFunction.NAME, new PercentFunction<>(random)); + schemaFunctions.put(PrebidKeyFunction.NAME, new PrebidKeyFunction()); + schemaFunctions.put(TcfInScopeFunction.NAME, new TcfInScopeFunction()); + schemaFunctions.put(UserFpdAvailableFunction.NAME, new UserFpdAvailableFunction()); + + resultFunctions = Map.of( + IncludeBiddersFunction.NAME, new IncludeBiddersFunction(mapper, bidderCatalog), + ExcludeBiddersFunction.NAME, new ExcludeBiddersFunction(mapper, bidderCatalog), + LogATagFunction.NAME, new LogATagFunction(mapper)); + } + + public SchemaFunction schemaFunctionByName(String name) { + final SchemaFunction function = schemaFunctions.get(name); + if (function == null) { + throw new InvalidSchemaFunctionException(name); + } + + return function; + } + + public ResultFunction resultFunctionByName(String name) { + final ResultFunction function = resultFunctions.get(name); + if (function == null) { + throw new InvalidResultFunctionException(name); + } + + return function; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java new file mode 100644 index 00000000000..0f20384d401 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; + +public class AnalyticsMapper { + + private static final String ACTIVITY_NAME = "pb-rule-engine"; + private static final String SUCCESS_STATUS = "success"; + + private AnalyticsMapper() { + } + + public static Tags toTags(ObjectMapper mapper, + String functionName, + List seatNonBids, + InfrastructureArguments infrastructureArguments, + String analyticsValue) { + + final String analyticsKey = infrastructureArguments.getAnalyticsKey(); + if (StringUtils.isEmpty(analyticsKey)) { + return TagsImpl.of(Collections.emptyList()); + } + + final List removedBidders = seatNonBids.stream() + .map(SeatNonBid::getSeat) + .distinct() + .toList(); + if (CollectionUtils.isEmpty(removedBidders)) { + return TagsImpl.of(Collections.emptyList()); + } + + final List impIds = seatNonBids.stream() + .flatMap(seatNonBid -> seatNonBid.getNonBid().stream()) + .map(NonBid::getImpId) + .distinct() + .toList(); + + final BidRejectionReason reason = seatNonBids.stream() + .flatMap(seatNonBid -> seatNonBid.getNonBid().stream()) + .map(NonBid::getStatusCode) + .findAny() + .orElse(null); + + final AnalyticsData analyticsData = new AnalyticsData( + analyticsKey, + analyticsValue, + infrastructureArguments.getModelVersion(), + infrastructureArguments.getRuleFired(), + functionName, + removedBidders, + reason); + + final Result result = ResultImpl.of( + SUCCESS_STATUS, mapper.valueToTree(analyticsData), AppliedToImpl.builder().impIds(impIds).build()); + + return TagsImpl.of(Collections.singletonList( + ActivityImpl.of(ACTIVITY_NAME, SUCCESS_STATUS, Collections.singletonList(result)))); + } + + private record AnalyticsData(@JsonProperty("analyticsKey") String analyticsKey, + @JsonProperty("analyticsValue") String analyticsValue, + @JsonProperty("modelVersion") String modelVersion, + @JsonProperty("conditionFired") String conditionFired, + @JsonProperty("resultFunction") String resultFunction, + @JsonProperty("biddersRemoved") List biddersRemoved, + @JsonProperty("seatNonBid") BidRejectionReason seatNonBid) { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java new file mode 100644 index 00000000000..b954abd664a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.util.StreamUtil; + +import java.util.Set; +import java.util.stream.Collectors; + +public class ExcludeBiddersFunction extends FilterBiddersFunction { + + public static final String NAME = "excludeBidders"; + + public ExcludeBiddersFunction(ObjectMapper objectMapper, BidderCatalog bidderCatalog) { + super(objectMapper, bidderCatalog); + } + + @Override + protected Set biddersToRemove(Imp imp, Boolean ifSyncedId, Set bidders, UidsCookie uidsCookie) { + final ObjectNode biddersNode = FilterUtils.bidderNode(imp.getExt()); + + return StreamUtil.asStream(biddersNode.fieldNames()) + .filter(bidder -> FilterUtils.containsIgnoreCase(bidders.stream(), bidder)) + .filter(bidder -> + ifSyncedId == null || ifSyncedId == isBidderIdSynced(bidder.toLowerCase(), uidsCookie)) + .collect(Collectors.toSet()); + } + + @Override + protected String name() { + return NAME; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java new file mode 100644 index 00000000000..8e2887c4ed8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java @@ -0,0 +1,148 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public abstract class FilterBiddersFunction implements ResultFunction { + + private final ObjectMapper mapper; + protected final BidderCatalog bidderCatalog; + + public FilterBiddersFunction(ObjectMapper mapper, BidderCatalog bidderCatalog) { + this.mapper = Objects.requireNonNull(mapper); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public RuleResult apply(ResultFunctionArguments arguments) { + final FilterBiddersFunctionConfig config = parseConfig(arguments.getConfig()); + + final BidRequest bidRequest = arguments.getOperand(); + final InfrastructureArguments infrastructureArguments = + arguments.getInfrastructureArguments(); + + final UidsCookie uidsCookie = infrastructureArguments.getContext().getAuctionContext().getUidsCookie(); + final Boolean ifSyncedId = config.getIfSyncedId(); + final BidRejectionReason rejectionReason = config.getSeatNonBid(); + final Granularity granularity = infrastructureArguments.getContext().getGranularity(); + + final List updatedImps = new ArrayList<>(); + final List seatNonBid = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + if (granularity instanceof Granularity.Imp(String impId) && !StringUtils.equals(impId, imp.getId())) { + updatedImps.add(imp); + continue; + } + + switch (filterBidders(imp, config.getBidders(), ifSyncedId, uidsCookie)) { + case FilterBiddersResult.NoAction noAction -> updatedImps.add(imp); + + case FilterBiddersResult.Reject reject -> + seatNonBid.addAll(toSeatNonBid(imp.getId(), reject.bidders(), rejectionReason)); + + case FilterBiddersResult.Update update -> { + updatedImps.add(update.imp()); + seatNonBid.addAll(toSeatNonBid(imp.getId(), update.bidders(), rejectionReason)); + } + } + } + + final Tags tags = AnalyticsMapper.toTags( + mapper, name(), seatNonBid, infrastructureArguments, config.getAnalyticsValue()); + + if (updatedImps.isEmpty()) { + return RuleResult.rejected(tags, seatNonBid); + } + + final RuleAction action = !seatNonBid.isEmpty() ? RuleAction.UPDATE : RuleAction.NO_ACTION; + final BidRequest result = action == RuleAction.UPDATE + ? bidRequest.toBuilder().imp(updatedImps).build() + : bidRequest; + + return RuleResult.of(result, action, tags, seatNonBid); + } + + private static List toSeatNonBid(String impId, Set bidders, BidRejectionReason reason) { + return bidders.stream() + .map(bidder -> SeatNonBid.of(bidder, Collections.singletonList(NonBid.of(impId, reason)))) + .toList(); + } + + private FilterBiddersResult filterBidders(Imp imp, + Set bidders, + Boolean ifSyncedId, + UidsCookie uidsCookie) { + + final Set biddersToRemove = biddersToRemove(imp, ifSyncedId, bidders, uidsCookie); + if (biddersToRemove.isEmpty()) { + return FilterBiddersResult.NoAction.instance(); + } + + final ObjectNode updatedExt = imp.getExt().deepCopy(); + final ObjectNode updatedBiddersNode = FilterUtils.bidderNode(updatedExt); + biddersToRemove.forEach(updatedBiddersNode::remove); + + return updatedBiddersNode.isEmpty() + ? new FilterBiddersResult.Reject(biddersToRemove) + : new FilterBiddersResult.Update(imp.toBuilder().ext(updatedExt).build(), biddersToRemove); + } + + protected abstract Set biddersToRemove(Imp imp, + Boolean ifSyncedId, + Set bidders, + UidsCookie uidsCookie); + + protected boolean isBidderIdSynced(String bidder, UidsCookie uidsCookie) { + return bidderCatalog.cookieFamilyName(bidder) + .map(uidsCookie::hasLiveUidFrom) + .orElse(false); + } + + @Override + public void validateConfig(ObjectNode config) { + final FilterBiddersFunctionConfig parsedConfig = parseConfig(config); + if (parsedConfig == null) { + throw new ConfigurationValidationException("Configuration is required, but not provided"); + } + + if (CollectionUtils.isEmpty(parsedConfig.getBidders())) { + throw new ConfigurationValidationException("'bidders' field is required"); + } + } + + private FilterBiddersFunctionConfig parseConfig(ObjectNode config) { + try { + return mapper.treeToValue(config, FilterBiddersFunctionConfig.class); + } catch (JsonProcessingException e) { + throw new ConfigurationValidationException(e.getMessage()); + } + } + + protected abstract String name(); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java new file mode 100644 index 00000000000..28ba0f5e518 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import org.prebid.server.auction.model.BidRejectionReason; + +import java.util.Set; + +@Value +@Builder +@Jacksonized +public class FilterBiddersFunctionConfig { + + Set bidders; + + @Builder.Default + @JsonProperty("seatnonbid") + BidRejectionReason seatNonBid = BidRejectionReason.REQUEST_BLOCKED_OPTIMIZED; + + @JsonProperty("ifSyncedId") + Boolean ifSyncedId; + + @JsonProperty("analyticsValue") + String analyticsValue; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java new file mode 100644 index 00000000000..48f71682f84 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java @@ -0,0 +1,22 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.iab.openrtb.request.Imp; + +import java.util.Set; + +public sealed interface FilterBiddersResult { + + record NoAction() implements FilterBiddersResult { + private static final NoAction INSTANCE = new NoAction(); + + public static NoAction instance() { + return INSTANCE; + } + } + + record Update(Imp imp, Set bidders) implements FilterBiddersResult { + } + + record Reject(Set bidders) implements FilterBiddersResult { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java new file mode 100644 index 00000000000..d3355c04bd7 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Optional; +import java.util.stream.Stream; + +public class FilterUtils { + + private static final String PREBID = "prebid"; + private static final String BIDDER = "bidder"; + + private FilterUtils() { + } + + public static ObjectNode bidderNode(ObjectNode impExt) { + return Optional.ofNullable(impExt.get(PREBID)) + .filter(JsonNode::isObject) + .map(prebidNode -> (ObjectNode) prebidNode) + .map(prebidNode -> prebidNode.get(BIDDER)) + .filter(JsonNode::isObject) + .map(bidderNode -> (ObjectNode) bidderNode) + .orElseThrow(() -> new IllegalStateException("Impression without ext.prebid.bidder")); + } + + public static boolean containsIgnoreCase(Stream stream, String value) { + return stream.anyMatch(value::equalsIgnoreCase); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java new file mode 100644 index 00000000000..08ea8f2b223 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.util.StreamUtil; + +import java.util.Set; +import java.util.stream.Collectors; + +public class IncludeBiddersFunction extends FilterBiddersFunction { + + public static final String NAME = "includeBidders"; + + public IncludeBiddersFunction(ObjectMapper objectMapper, BidderCatalog bidderCatalog) { + super(objectMapper, bidderCatalog); + } + + @Override + protected Set biddersToRemove(Imp imp, Boolean ifSyncedId, Set bidders, UidsCookie uidsCookie) { + final ObjectNode biddersNode = FilterUtils.bidderNode(imp.getExt()); + + return StreamUtil.asStream(biddersNode.fieldNames()) + .filter(bidder -> !FilterUtils.containsIgnoreCase(bidders.stream(), bidder) + || (ifSyncedId != null && ifSyncedId != isBidderIdSynced(bidder.toLowerCase(), uidsCookie))) + .collect(Collectors.toSet()); + } + + @Override + protected String name() { + return NAME; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java new file mode 100644 index 00000000000..68c78e65d53 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java @@ -0,0 +1,61 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.Collections; +import java.util.List; + +public class AnalyticsMapper { + + private static final String ACTIVITY_NAME = "pb-rule-engine"; + private static final String SUCCESS_STATUS = "success"; + + private AnalyticsMapper() { + } + + public static Tags toTags(ObjectMapper mapper, + InfrastructureArguments infrastructureArguments, + String analyticsValue) { + + final String analyticsKey = infrastructureArguments.getAnalyticsKey(); + if (StringUtils.isEmpty(analyticsKey)) { + return TagsImpl.of(Collections.emptyList()); + } + + final AnalyticsData analyticsData = new AnalyticsData( + analyticsKey, + analyticsValue, + infrastructureArguments.getModelVersion(), + infrastructureArguments.getRuleFired(), + LogATagFunction.NAME); + + final Granularity granularity = infrastructureArguments.getContext().getGranularity(); + final List impIds = granularity instanceof Granularity.Imp(String impId) + ? Collections.singletonList(impId) + : Collections.singletonList("*"); + + final Result result = ResultImpl.of( + SUCCESS_STATUS, mapper.valueToTree(analyticsData), AppliedToImpl.builder().impIds(impIds).build()); + + return TagsImpl.of(Collections.singletonList( + ActivityImpl.of(ACTIVITY_NAME, SUCCESS_STATUS, Collections.singletonList(result)))); + } + + private record AnalyticsData(@JsonProperty("analyticsKey") String analyticsKey, + @JsonProperty("analyticsValue") String analyticsValue, + @JsonProperty("modelVersion") String modelVersion, + @JsonProperty("conditionFired") String conditionFired, + @JsonProperty("resultFunction") String resultFunction) { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java new file mode 100644 index 00000000000..fe7b7543302 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java @@ -0,0 +1,43 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.Collections; +import java.util.Objects; + +public class LogATagFunction implements ResultFunction { + + public static final String NAME = "logAtag"; + + private static final String ANALYTICS_VALUE_FIELD = "analyticsValue"; + + private final ObjectMapper mapper; + + public LogATagFunction(ObjectMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public RuleResult apply(ResultFunctionArguments arguments) { + final Tags tags = AnalyticsMapper.toTags( + mapper, + arguments.getInfrastructureArguments(), + arguments.getConfig().get(ANALYTICS_VALUE_FIELD).asText()); + + return RuleResult.of(arguments.getOperand(), RuleAction.NO_ACTION, tags, Collections.emptyList()); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertString(config, ANALYTICS_VALUE_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java new file mode 100644 index 00000000000..165293f8b71 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.AdUnitCodeUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +public class AdUnitCodeFunction implements SchemaFunction { + + public static final String NAME = "adUnitCode"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final RequestRuleContext context = arguments.getContext(); + final String impId = ((Granularity.Imp) context.getGranularity()).impId(); + final BidRequest bidRequest = arguments.getOperand(); + + return ListUtils.emptyIfNull(bidRequest.getImp()).stream() + .filter(imp -> StringUtils.equals(imp.getId(), impId)) + .findFirst() + .flatMap(AdUnitCodeUtils::extractAdUnitCode) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java new file mode 100644 index 00000000000..6b015863f97 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java @@ -0,0 +1,60 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.AdUnitCodeUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class AdUnitCodeInFunction implements SchemaFunction { + + public static final String NAME = "adUnitCodeIn"; + + private static final String CODES_FIELD = "codes"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final RequestRuleContext context = arguments.getContext(); + final String impId = ((Granularity.Imp) context.getGranularity()).impId(); + final BidRequest bidRequest = arguments.getOperand(); + + final Imp adUnit = ListUtils.emptyIfNull(bidRequest.getImp()).stream() + .filter(imp -> StringUtils.equals(imp.getId(), impId)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Critical error in rules engine. Imp id of absent imp supplied")); + + final Set adUnitPotentialCodes = Stream.of( + AdUnitCodeUtils.extractGpid(adUnit), + AdUnitCodeUtils.extractTagId(adUnit), + AdUnitCodeUtils.extractPbAdSlot(adUnit), + AdUnitCodeUtils.extractStoredRequestId(adUnit)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(CODES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(adUnitPotentialCodes::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, CODES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java new file mode 100644 index 00000000000..c99f0b618a0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class BundleFunction implements SchemaFunction { + + public static final String NAME = "bundle"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.ofNullable(arguments.getOperand().getApp()) + .map(App::getBundle) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java new file mode 100644 index 00000000000..9389d686938 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; + +public class BundleInFunction implements SchemaFunction { + + public static final String NAME = "bundleIn"; + + private static final String BUNDLES_FIELD = "bundles"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final String bundle = Optional.ofNullable(arguments.getOperand().getApp()) + .map(App::getBundle) + .orElse(UNDEFINED_RESULT); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(BUNDLES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(bundle::equals); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, BUNDLES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java new file mode 100644 index 00000000000..2c8a5fd94aa --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; + +import java.util.Optional; + +public class ChannelFunction implements SchemaFunction { + + public static final String NAME = "channel"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.of(arguments.getOperand()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getChannel) + .map(ExtRequestPrebidChannel::getName) + .map(ChannelFunction::resolveChannel) + .orElse(SchemaFunction.UNDEFINED_RESULT); + } + + private static String resolveChannel(String channel) { + return channel.equals("pbjs") ? "web" : channel; + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java new file mode 100644 index 00000000000..6d3e7acc7c1 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +public class DataCenterFunction implements SchemaFunction { + + public static final String NAME = "dataCenter"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return StringUtils.defaultIfEmpty(arguments.getContext().getDatacenter(), UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java new file mode 100644 index 00000000000..8679d9c70e3 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +public class DataCenterInFunction implements SchemaFunction { + + public static final String NAME = "dataCenterIn"; + + private static final String DATACENTERS_FIELD = "datacenters"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final String datacenter = StringUtils.defaultIfEmpty(arguments.getContext().getDatacenter(), UNDEFINED_RESULT); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(DATACENTERS_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(datacenter::equalsIgnoreCase); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, DATACENTERS_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java new file mode 100644 index 00000000000..f2444f58985 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class DeviceCountryFunction implements SchemaFunction { + + public static final String NAME = "deviceCountry"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.of(arguments.getOperand()) + .map(BidRequest::getDevice) + .map(Device::getGeo) + .map(Geo::getCountry) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java new file mode 100644 index 00000000000..d2dbf5904ef --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java @@ -0,0 +1,40 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; + +public class DeviceCountryInFunction implements SchemaFunction { + + public static final String NAME = "deviceCountryIn"; + private static final String COUNTRIES_FIELD = "countries"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final String deviceCountry = Optional.of(arguments.getOperand()) + .map(BidRequest::getDevice) + .map(Device::getGeo) + .map(Geo::getCountry) + .orElse(UNDEFINED_RESULT); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(COUNTRIES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(deviceCountry::equalsIgnoreCase); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, COUNTRIES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java new file mode 100644 index 00000000000..490db16a351 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class DeviceTypeFunction implements SchemaFunction { + + public static final String NAME = "deviceType"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.ofNullable(arguments.getOperand().getDevice()) + .map(Device::getDevicetype) + .map(String::valueOf) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java new file mode 100644 index 00000000000..daacb7ad434 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java @@ -0,0 +1,42 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; + +public class DeviceTypeInFunction implements SchemaFunction { + + public static final String NAME = "deviceTypeIn"; + + public static final String TYPES_FIELD = "types"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Integer deviceType = Optional.ofNullable(arguments.getOperand().getDevice()) + .map(Device::getDevicetype) + .orElse(null); + + if (deviceType == null) { + return Boolean.FALSE.toString(); + } + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(TYPES_FIELD).elements()) + .map(JsonNode::asInt) + .anyMatch(deviceType::equals); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfIntegers(config, TYPES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java new file mode 100644 index 00000000000..0fb1e39082f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.DomainUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +public class DomainFunction implements SchemaFunction { + + public static final String NAME = "domain"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return DomainUtils.extractDomain(arguments.getOperand()) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java new file mode 100644 index 00000000000..bfd07a13ea9 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java @@ -0,0 +1,50 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.DomainUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DomainInFunction implements SchemaFunction { + + public static final String NAME = "domainIn"; + + private static final String DOMAINS_FIELD = "domains"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final BidRequest bidRequest = arguments.getOperand(); + + final Set suppliedDomains = Stream.of( + DomainUtils.extractSitePublisherDomain(bidRequest), + DomainUtils.extractAppPublisherDomain(bidRequest), + DomainUtils.extractDoohPublisherDomain(bidRequest), + DomainUtils.extractSiteDomain(bidRequest), + DomainUtils.extractAppDomain(bidRequest), + DomainUtils.extractDoohDomain(bidRequest)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(DOMAINS_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(suppliedDomains::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, DOMAINS_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java new file mode 100644 index 00000000000..9c9c8fb2a99 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java @@ -0,0 +1,33 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class EidAvailableFunction implements SchemaFunction { + + public static final String NAME = "eidAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final boolean available = Optional.of(arguments.getOperand()) + .map(BidRequest::getUser) + .map(User::getEids) + .filter(ListUtil::isNotEmpty) + .isPresent(); + + return Boolean.toString(available); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java new file mode 100644 index 00000000000..6c9e4d0070f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java @@ -0,0 +1,46 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class EidInFunction implements SchemaFunction { + + public static final String NAME = "eidIn"; + + private static final String SOURCES_FIELD = "sources"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Set sources = Optional.of(arguments.getOperand()) + .map(BidRequest::getUser) + .map(User::getEids) + .stream() + .flatMap(Collection::stream) + .map(Eid::getSource) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(SOURCES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(sources::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, SOURCES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java new file mode 100644 index 00000000000..66c19e14b36 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java @@ -0,0 +1,90 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Optional; +import java.util.function.Predicate; + +public class FpdAvailableFunction implements SchemaFunction { + + public static final String NAME = "fpdAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final BidRequest bidRequest = arguments.getOperand(); + + final boolean available = isUserDataAvailable(bidRequest) + || isUserExtDataAvailable(bidRequest) + || isSiteContentDataAvailable(bidRequest) + || isSiteExtDataAvailable(bidRequest) + || isAppContentDataAvailable(bidRequest) + || isAppExtDataAvailable(bidRequest); + + return Boolean.toString(available); + } + + private static boolean isUserDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getUser()) + .map(User::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + } + + private static boolean isUserExtDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ExtUser::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + } + + private static boolean isSiteContentDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()) + .map(Site::getContent) + .map(Content::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + } + + private static boolean isSiteExtDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()) + .map(Site::getExt) + .map(ExtSite::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + } + + private static boolean isAppContentDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()) + .map(App::getContent) + .map(Content::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + } + + private static boolean isAppExtDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()) + .map(App::getExt) + .map(ExtApp::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java new file mode 100644 index 00000000000..ef306e65351 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; + +public class GppSidAvailableFunction implements SchemaFunction { + + public static final String NAME = "gppSidAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final boolean available = Optional.of(arguments.getOperand()) + .map(BidRequest::getRegs) + .map(Regs::getGppSid) + .stream() + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .anyMatch(sid -> sid > 0); + + return Boolean.toString(available); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java new file mode 100644 index 00000000000..46bf0aad29c --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java @@ -0,0 +1,44 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class GppSidInFunction implements SchemaFunction { + + public static final String NAME = "gppSidIn"; + + private static final String SIDS_FIELD = "sids"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Set sids = Optional.of(arguments.getOperand()) + .map(BidRequest::getRegs) + .map(Regs::getGppSid) + .stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(SIDS_FIELD).elements()) + .map(JsonNode::asInt) + .anyMatch(sids::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfIntegers(config, SIDS_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java new file mode 100644 index 00000000000..547efb749ee --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java @@ -0,0 +1,70 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.spring.config.bidder.model.MediaType; +import org.prebid.server.util.StreamUtil; + +import java.util.HashSet; +import java.util.Set; + +public class MediaTypeInFunction implements SchemaFunction { + + public static final String NAME = "mediaTypeIn"; + + private static final String TYPES_FIELD = "types"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final RequestRuleContext context = arguments.getContext(); + final String impId = ((Granularity.Imp) context.getGranularity()).impId(); + final BidRequest bidRequest = arguments.getOperand(); + + final Imp adUnit = ListUtils.emptyIfNull(bidRequest.getImp()).stream() + .filter(imp -> StringUtils.equals(imp.getId(), impId)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Critical error in rules engine. Imp id of absent imp supplied")); + + final Set adUnitMediaTypes = adUnitMediaTypes(adUnit); + + final boolean intersects = StreamUtil.asStream(arguments.getConfig().get(TYPES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(adUnitMediaTypes::contains); + + return Boolean.toString(intersects); + } + + private static Set adUnitMediaTypes(Imp imp) { + final Set result = new HashSet<>(); + + if (imp.getBanner() != null) { + result.add(MediaType.BANNER.getKey()); + } + if (imp.getVideo() != null) { + result.add(MediaType.VIDEO.getKey()); + } + if (imp.getAudio() != null) { + result.add(MediaType.AUDIO.getKey()); + } + if (imp.getXNative() != null) { + result.add(MediaType.NATIVE.getKey()); + } + + return result; + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, TYPES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java new file mode 100644 index 00000000000..17c42beb5b5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Optional; + +public class PrebidKeyFunction implements SchemaFunction { + + public static final String NAME = "prebidKey"; + + private static final String KEY_FIELD = "key"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.ofNullable(arguments.getOperand().getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getKvps) + .map(kvps -> kvps.get(arguments.getConfig().get(KEY_FIELD).asText())) + .filter(JsonNode::isTextual) + .map(JsonNode::asText) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertString(config, KEY_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java new file mode 100644 index 00000000000..b98d9672a44 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class TcfInScopeFunction implements SchemaFunction { + + public static final String NAME = "tcfInScope"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final boolean inScope = Optional.of(arguments.getOperand()) + .map(BidRequest::getRegs) + .map(Regs::getGdpr) + .filter(Integer.valueOf(1)::equals) + .isPresent(); + + return Boolean.toString(inScope); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java new file mode 100644 index 00000000000..2a4ecc83206 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java @@ -0,0 +1,41 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Optional; +import java.util.function.Predicate; + +public class UserFpdAvailableFunction implements SchemaFunction { + + public static final String NAME = "userFpdAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Optional user = Optional.of(arguments.getOperand()) + .map(BidRequest::getUser); + + final boolean userDataAvailable = user.map(User::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + + final boolean userExtDataAvailable = user.map(User::getExt) + .map(ExtUser::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + + return Boolean.toString(userDataAvailable || userExtDataAvailable); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java new file mode 100644 index 00000000000..1d7c8093fa6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java @@ -0,0 +1,49 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; + +import java.util.Optional; + +public class AdUnitCodeUtils { + + private AdUnitCodeUtils() { + } + + public static Optional extractAdUnitCode(Imp imp) { + return extractGpid(imp) + .or(() -> extractTagId(imp)) + .or(() -> extractPbAdSlot(imp)) + .or(() -> extractStoredRequestId(imp)); + } + + public static Optional extractGpid(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("gpid")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText); + } + + public static Optional extractTagId(Imp imp) { + return Optional.ofNullable(imp.getTagid()) + .filter(StringUtils::isNotBlank); + } + + public static Optional extractPbAdSlot(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("data")) + .map(data -> data.get("pbadslot")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText); + } + + public static Optional extractStoredRequestId(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("prebid")) + .map(prebid -> prebid.get("storedrequest")) + .map(storedRequest -> storedRequest.get("id")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java new file mode 100644 index 00000000000..b9d57ef8aed --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; + +import java.util.Optional; + +public class DomainUtils { + + private DomainUtils() { + } + + public static Optional extractDomain(BidRequest bidRequest) { + return extractPublisherDomain(bidRequest) + .or(() -> extractPlainDomain(bidRequest)); + } + + private static Optional extractPublisherDomain(BidRequest bidRequest) { + return extractSitePublisherDomain(bidRequest) + .or(() -> extractAppPublisherDomain(bidRequest)) + .or(() -> extractDoohPublisherDomain(bidRequest)); + } + + public static Optional extractSitePublisherDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()) + .map(Site::getPublisher) + .map(Publisher::getDomain); + } + + public static Optional extractAppPublisherDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()) + .map(App::getPublisher) + .map(Publisher::getDomain); + } + + public static Optional extractDoohPublisherDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getDooh()) + .map(Dooh::getPublisher) + .map(Publisher::getDomain); + } + + private static Optional extractPlainDomain(BidRequest bidRequest) { + return extractSiteDomain(bidRequest) + .or(() -> extractAppDomain(bidRequest)) + .or(() -> extractDoohDomain(bidRequest)); + } + + public static Optional extractSiteDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()).map(Site::getDomain); + } + + public static Optional extractAppDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()).map(App::getDomain); + } + + public static Optional extractDoohDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getDooh()).map(Dooh::getDomain); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java new file mode 100644 index 00000000000..4370ef58502 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +@Value(staticConstructor = "of") +public class AlternativeActionRule implements Rule { + + Rule delegate; + Rule alternative; + + public RuleResult process(T value, C context) { + try { + return delegate.process(value, context); + } catch (NoMatchingRuleException e) { + return alternative.process(value, context); + } + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java new file mode 100644 index 00000000000..ca922f1720a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CompositeRule implements Rule { + + List> subrules; + + @Override + public RuleResult process(T value, C context) { + RuleResult result = RuleResult.noAction(value); + + for (Rule subrule : subrules) { + result = result.mergeWith(subrule.process(value, context)); + + if (result.isReject()) { + return result; + } + } + + return result; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java new file mode 100644 index 00000000000..c56c580d149 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java @@ -0,0 +1,90 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.LookupResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ConditionalRule implements Rule { + + private final Schema schema; + private final RuleTree> ruleTree; + + private final String modelVersion; + private final String analyticsKey; + + public ConditionalRule(Schema schema, + RuleTree> ruleTree, + String analyticsKey, + String modelVersion) { + + this.schema = Objects.requireNonNull(schema); + + this.ruleTree = Objects.requireNonNull(ruleTree); + this.analyticsKey = StringUtils.defaultString(analyticsKey); + this.modelVersion = StringUtils.defaultString(modelVersion); + } + + @Override + public RuleResult process(T value, C context) { + final List> schemaFunctions = schema.getFunctions(); + final List matchers = schemaFunctions.stream() + .map(holder -> holder.getSchemaFunction().extract( + SchemaFunctionArguments.of(value, holder.getConfig(), context))) + .map(matcher -> StringUtils.defaultIfEmpty(matcher, SchemaFunction.UNDEFINED_RESULT)) + .toList(); + + final LookupResult> lookupResult = ruleTree.lookup(matchers); + final RuleConfig ruleConfig = lookupResult.getValue(); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(mergeWithSchema(schema, matchers)) + .schemaFunctionMatches(mergeWithSchema(schema, lookupResult.getMatches())) + .ruleFired(ruleConfig.getCondition()) + .analyticsKey(analyticsKey) + .modelVersion(modelVersion) + .build(); + + RuleResult result = RuleResult.noAction(value); + for (ResultFunctionHolder action : ruleConfig.getActions()) { + result = result.mergeWith(applyAction(action, result.getValue(), infrastructureArguments)); + + if (result.isReject()) { + return result; + } + } + + return result; + } + + private Map mergeWithSchema(Schema schema, List values) { + return IntStream.range(0, values.size()) + .boxed() + .collect(Collectors.toMap( + idx -> schema.getFunctions().get(idx).getName(), values::get, (left, right) -> left)); + } + + private RuleResult applyAction(ResultFunctionHolder action, + T value, + InfrastructureArguments infrastructureArguments) { + + final ResultFunctionArguments arguments = ResultFunctionArguments.of( + value, action.getConfig(), infrastructureArguments); + + return action.getFunction().apply(arguments); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java new file mode 100644 index 00000000000..918b544e086 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +public interface ConditionalRuleFactory { + + Rule create(Schema schema, + RuleTree> ruleTree, + String analyticsKey, + String modelVersion); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java new file mode 100644 index 00000000000..5e3198b8e24 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.EqualsAndHashCode; +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@EqualsAndHashCode +public class DefaultActionRule implements Rule { + + private static final String RULE_NAME = "default"; + + private final List> actions; + + private final String analyticsKey; + private final String modelVersion; + + public DefaultActionRule(List> actions, String analyticsKey, String modelVersion) { + this.actions = ListUtils.emptyIfNull(actions); + + this.analyticsKey = Objects.requireNonNull(analyticsKey); + this.modelVersion = Objects.requireNonNull(modelVersion); + } + + @Override + public RuleResult process(T value, C context) { + RuleResult result = RuleResult.noAction(value); + + for (ResultFunctionHolder action : actions) { + result = result.mergeWith(applyAction(action, result.getValue(), context)); + + if (result.isReject()) { + return result; + } + } + + return result; + } + + private RuleResult applyAction(ResultFunctionHolder action, T value, C context) { + final ResultFunctionArguments arguments = ResultFunctionArguments.of( + value, action.getConfig(), infrastructureArguments(context)); + + return action.getFunction().apply(arguments); + } + + private InfrastructureArguments infrastructureArguments(C context) { + return InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired(RULE_NAME) + .analyticsKey(analyticsKey) + .modelVersion(modelVersion) + .build(); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java new file mode 100644 index 00000000000..c9ea02dc9f4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; + +@Value(staticConstructor = "create") +public class NoOpRule implements Rule { + + @Override + public RuleResult process(T value, C context) { + return RuleResult.noAction(value); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java new file mode 100644 index 00000000000..c9d3c3383be --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import com.iab.openrtb.request.BidRequest; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; + +import java.time.Instant; + +@Builder +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class PerStageRule { + + private static final PerStageRule NO_OP = PerStageRule.builder() + .processedAuctionRequestRule(NoOpRule.create()) + .build(); + + Instant timestamp; + + Rule processedAuctionRequestRule; + + public static PerStageRule noOp() { + return NO_OP; + } +} + diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java new file mode 100644 index 00000000000..e3275ef0621 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; + +import java.util.random.RandomGenerator; + +@Value(staticConstructor = "of") +public class RandomWeightedRule implements Rule { + + RandomGenerator random; + WeightedList> weightedList; + + @Override + public RuleResult process(T value, C context) { + return weightedList.getForSeed(random.nextInt(weightedList.maxSeed())).process(value, context); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java new file mode 100644 index 00000000000..1b369d52d9b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +public interface Rule { + + RuleResult process(T value, C context); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java new file mode 100644 index 00000000000..b123850bebb --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +public enum RuleAction { + + NO_ACTION, UPDATE, REJECT +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java new file mode 100644 index 00000000000..0c40c198e7c --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; + +import java.util.List; + +@Value(staticConstructor = "of") +public class RuleConfig { + + String condition; + + List> actions; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java new file mode 100644 index 00000000000..8967430982b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; + +@Value(staticConstructor = "of") +public class RuleResult { + + T value; + + RuleAction action; + + Tags analyticsTags; + + List seatNonBid; + + public RuleResult mergeWith(RuleResult other) { + final T value = other.getValue(); + final RuleAction action = merge(this.action, other.getAction()); + final Tags tags = TagsImpl.of(ListUtil.union(analyticsTags.activities(), other.analyticsTags.activities())); + final List seatNonBids = ListUtil.union(seatNonBid, other.seatNonBid); + + return RuleResult.of(value, action, tags, seatNonBids); + } + + private static RuleAction merge(RuleAction left, RuleAction right) { + if (left == RuleAction.REJECT || right == RuleAction.REJECT) { + return RuleAction.REJECT; + } + if (left == RuleAction.UPDATE || right == RuleAction.UPDATE) { + return RuleAction.UPDATE; + } + return RuleAction.NO_ACTION; + } + + public boolean isReject() { + return action == RuleAction.REJECT; + } + + public boolean isUpdate() { + return action == RuleAction.UPDATE; + } + + public static RuleResult noAction(T value) { + return RuleResult.of( + value, RuleAction.NO_ACTION, TagsImpl.of(Collections.emptyList()), Collections.emptyList()); + } + + public static RuleResult rejected(Tags analyticsTags, List seatNonBids) { + return RuleResult.of(null, RuleAction.REJECT, analyticsTags, seatNonBids); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java new file mode 100644 index 00000000000..2083983091f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; + +public interface StageSpecification { + + SchemaFunction schemaFunctionByName(String name); + + ResultFunction resultFunctionByName(String name); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java new file mode 100644 index 00000000000..e7fea8db54d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +public class InvalidMatcherConfiguration extends RuntimeException { + + public InvalidMatcherConfiguration(String message) { + super(message); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java new file mode 100644 index 00000000000..356f7ef53b4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +public class InvalidResultFunctionException extends RuntimeException { + + public InvalidResultFunctionException(String function) { + super("Invalid result function: " + function); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java new file mode 100644 index 00000000000..f1b0f05f7b6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +public class InvalidSchemaFunctionException extends RuntimeException { + + public InvalidSchemaFunctionException(String function) { + super("Invalid schema function: " + function); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java new file mode 100644 index 00000000000..fc1869d3d14 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +import org.prebid.server.hooks.modules.rule.engine.core.util.NoMatchingValueException; + +public class NoMatchingRuleException extends NoMatchingValueException { + + public NoMatchingRuleException() { + super("No matching rule found"); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java new file mode 100644 index 00000000000..f30eb769ddf --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Value +@Builder +public class InfrastructureArguments { + + C context; + + Map schemaFunctionResults; + + Map schemaFunctionMatches; + + String ruleFired; + + String analyticsKey; + + String modelVersion; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java new file mode 100644 index 00000000000..831a85c6af9 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; + +public interface ResultFunction { + + RuleResult apply(ResultFunctionArguments arguments); + + void validateConfig(ObjectNode config); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java new file mode 100644 index 00000000000..abe28017df8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ResultFunctionArguments { + + T operand; + + ObjectNode config; + + InfrastructureArguments infrastructureArguments; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java new file mode 100644 index 00000000000..5b7476511d4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ResultFunctionHolder { + + String name; + + ResultFunction function; + + ObjectNode config; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java new file mode 100644 index 00000000000..64ec5aa6e23 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class Schema { + + List> functions; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java new file mode 100644 index 00000000000..887d76eb9ec --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public interface SchemaFunction { + + String UNDEFINED_RESULT = "undefined"; + + String extract(SchemaFunctionArguments arguments); + + void validateConfig(ObjectNode config); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java new file mode 100644 index 00000000000..062d1776d0f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SchemaFunctionArguments { + + T operand; + + ObjectNode config; + + C context; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java new file mode 100644 index 00000000000..2264cfc2088 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SchemaFunctionHolder { + + String name; + + SchemaFunction schemaFunction; + + ObjectNode config; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/functions/PercentFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/functions/PercentFunction.java new file mode 100644 index 00000000000..2d03ce5e6e0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/functions/PercentFunction.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.random.RandomGenerator; + +@RequiredArgsConstructor +public class PercentFunction implements SchemaFunction { + + public static final String NAME = "percent"; + + private static final String PCT_FIELD = "pct"; + + private final RandomGenerator random; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final int resolvedUpperBound = Math.min(Math.max(arguments.getConfig().get(PCT_FIELD).asInt(), 0), 100); + return Boolean.toString(random.nextInt(100) < resolvedUpperBound); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertInteger(config, PCT_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/LookupResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/LookupResult.java new file mode 100644 index 00000000000..283b81a9650 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/LookupResult.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class LookupResult { + + T value; + + List matches; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleNode.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleNode.java new file mode 100644 index 00000000000..69487fd38b0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleNode.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import java.util.Map; + +public sealed interface RuleNode { + + record IntermediateNode(Map> children) implements RuleNode { + + public RuleNode next(String arg) { + return children.get(arg); + } + } + + record LeafNode(T value) implements RuleNode { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTree.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTree.java new file mode 100644 index 00000000000..6473bd5620f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTree.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import lombok.Getter; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class RuleTree { + + public static final String WILDCARD_MATCHER = "*"; + + private final RuleNode root; + + @Getter + private final int depth; + + public RuleTree(RuleNode root, int depth) { + this.root = Objects.requireNonNull(root); + this.depth = depth; + } + + public LookupResult lookup(List path) { + final List matches = new ArrayList<>(); + RuleNode next = root; + + for (String pathPart : path) { + next = switch (next) { + case RuleNode.IntermediateNode node -> { + final RuleNode result = node.next(pathPart); + matches.add(result == null ? WILDCARD_MATCHER : pathPart); + yield ObjectUtils.defaultIfNull(result, node.next(WILDCARD_MATCHER)); + } + + case RuleNode.LeafNode ignored -> throw new IllegalArgumentException("Argument count mismatch"); + case null -> throw new NoMatchingRuleException(); + }; + } + + return switch (next) { + case RuleNode.LeafNode leaf -> LookupResult.of(leaf.value(), matches); + case RuleNode.IntermediateNode ignored -> throw new IllegalArgumentException("Argument count mismatch"); + case null -> throw new NoMatchingRuleException(); + }; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeFactory.java new file mode 100644 index 00000000000..83acf0455c6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeFactory.java @@ -0,0 +1,89 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidMatcherConfiguration; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class RuleTreeFactory { + + private RuleTreeFactory() { + } + + public static RuleTree> buildTree(List> rules) { + final List>> parsingContexts = toParsingContexts(rules); + final int depth = getDepth(parsingContexts); + + if (depth == 0) { + throw new InvalidMatcherConfiguration("Rule with no matchers"); + } + + if (!parsingContexts.stream().allMatch(context -> context.argumentMatchers().size() == depth)) { + throw new InvalidMatcherConfiguration("Mismatched arguments count"); + } + + validateRules(parsingContexts); + + return new RuleTree<>(parseRuleNode(parsingContexts), depth); + } + + private static void validateRules(List>> parsingContexts) { + final List ambiguousRules = parsingContexts.stream() + .collect(Collectors.groupingBy(context -> context.value().getCondition(), Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .toList(); + + if (!ambiguousRules.isEmpty()) { + throw new InvalidMatcherConfiguration("Ambiguous matchers: " + String.join(", ", ambiguousRules)); + } + } + + private static List>> toParsingContexts(List> rules) { + return rules.stream() + .map(rule -> new ParsingContext<>( + List.of(StringUtils.defaultString(rule.getCondition()).split("\\|")), + rule)) + .toList(); + } + + private static int getDepth(List> contexts) { + return contexts.isEmpty() ? 0 : contexts.getFirst().argumentMatchers().size(); + } + + private static RuleNode parseRuleNode(List> parsingContexts) { + if (parsingContexts.size() == 1 && parsingContexts.getFirst().argumentMatchers().isEmpty()) { + return new RuleNode.LeafNode<>(parsingContexts.getFirst().value); + } + + final Map>> subrules = parsingContexts.stream() + .collect(Collectors.groupingBy( + ParsingContext::argumentMatcher, + Collectors.mapping(ParsingContext::next, Collectors.toList()))); + + final Map> parsedSubrules = subrules.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> parseRuleNode(entry.getValue()))); + + return new RuleNode.IntermediateNode<>(parsedSubrules); + } + + private record ParsingContext(List argumentMatchers, T value) { + + public ParsingContext next() { + return new ParsingContext<>(tail(argumentMatchers), value); + } + + public String argumentMatcher() { + return argumentMatchers.getFirst(); + } + } + + private static List tail(List list) { + return list.subList(1, list.size()); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ConfigurationValidationException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ConfigurationValidationException.java new file mode 100644 index 00000000000..6c5c194d3e1 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ConfigurationValidationException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +public class ConfigurationValidationException extends RuntimeException { + + public ConfigurationValidationException(String message) { + super(message); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ListUtil.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ListUtil.java new file mode 100644 index 00000000000..f1dd471b045 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ListUtil.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import java.util.Collection; +import java.util.Objects; + +public class ListUtil { + + private ListUtil() { + } + + public static boolean isNotEmpty(Collection collection) { + return collection != null && !collection.isEmpty() && collection.stream().anyMatch(Objects::nonNull); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/NoMatchingValueException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/NoMatchingValueException.java new file mode 100644 index 00000000000..3840cc14ff0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/NoMatchingValueException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +public class NoMatchingValueException extends RuntimeException { + + public NoMatchingValueException(String message) { + super(message); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ValidationUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ValidationUtils.java new file mode 100644 index 00000000000..b95663dfea4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ValidationUtils.java @@ -0,0 +1,69 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; +import java.util.function.Predicate; + +public class ValidationUtils { + + private ValidationUtils() { + } + + public static void assertArrayOfStrings(ObjectNode config, String fieldName) { + assertArrayOf( + config, + fieldName, + JsonNode::isTextual, + "Field '%s' is required and has to be an array of strings"); + } + + public static void assertArrayOfIntegers(ObjectNode config, String fieldName) { + assertArrayOf( + config, + fieldName, + JsonNode::isInt, + "Field '%s' is required and has to be an array of integers"); + } + + private static void assertArrayOf(ObjectNode config, + String fieldName, + Predicate predicate, + String messageTemplate) { + + Optional.ofNullable(config) + .map(node -> node.get(fieldName)) + .filter(JsonNode::isArray) + .map(node -> (ArrayNode) node) + .filter(Predicate.not(ArrayNode::isEmpty)) + .filter(node -> StreamUtil.asStream(node.elements()).allMatch(predicate)) + .orElseThrow(() -> new ConfigurationValidationException(messageTemplate.formatted(fieldName))); + } + + public static void assertString(ObjectNode config, String fieldName) { + assertField(config, fieldName, JsonNode::isTextual, "Field '%s' is required and has to be a string"); + } + + public static void assertInteger(ObjectNode config, String fieldName) { + assertField(config, fieldName, JsonNode::isInt, "Field '%s' is required and has to be an integer"); + } + + private static void assertField(ObjectNode config, + String fieldName, + Predicate predicate, + String messageTemplate) { + + if (config == null || !config.has(fieldName) || !predicate.test(config.get(fieldName))) { + throw new ConfigurationValidationException(messageTemplate.formatted(fieldName)); + } + } + + public static void assertNoArgs(ObjectNode config) { + if (config != null && !config.isEmpty()) { + throw new ConfigurationValidationException("No arguments allowed"); + } + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedEntry.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedEntry.java new file mode 100644 index 00000000000..977a7a6daab --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedEntry.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import lombok.Value; + +@Value +public class WeightedEntry { + + int weight; + + T value; + + private WeightedEntry(int weight, T value) { + this.weight = weight; + this.value = value; + + if (weight <= 0) { + throw new IllegalArgumentException("Weight must be greater than zero"); + } + } + + public static WeightedEntry of(int weight, T value) { + return new WeightedEntry<>(weight, value); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedList.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedList.java new file mode 100644 index 00000000000..92fcd2e455b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedList.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import lombok.EqualsAndHashCode; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +@EqualsAndHashCode +public class WeightedList { + + private final List> entries; + private final int weightSum; + + public WeightedList(List> entries) { + validateEntries(entries); + + this.weightSum = entries.stream().mapToInt(WeightedEntry::getWeight).sum(); + this.entries = prepareEntries(entries); + } + + private void validateEntries(List> entries) { + if (CollectionUtils.isEmpty(entries)) { + throw new IllegalArgumentException("Weighted list cannot be empty"); + } + } + + private List> prepareEntries(List> entries) { + final List> result = new ArrayList<>(entries.size()); + + int cumulativeSum = 0; + + for (WeightedEntry entry : entries) { + cumulativeSum += entry.getWeight(); + result.add(WeightedEntry.of(cumulativeSum, entry.getValue())); + } + + return result; + } + + public T getForSeed(int seed) { + if (seed < 0 || seed >= maxSeed()) { + throw new IllegalArgumentException("Seed number must be between 0 and " + weightSum); + } + + for (WeightedEntry entry : entries) { + if (seed < entry.getWeight()) { + return entry.getValue(); + } + } + + throw new NoMatchingValueException("No entry found for seed " + seed); + } + + public int maxSeed() { + return weightSum; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineModule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineModule.java new file mode 100644 index 00000000000..7ca7206116f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineModule.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.rule.engine.v1; + +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Collections; + +public class PbRuleEngineModule implements Module { + + public static final String CODE = "pb-rule-engine"; + + private final Collection> hooks; + + public PbRuleEngineModule(RuleParser ruleParser, String datacenter) { + this.hooks = Collections.singleton( + new PbRuleEngineProcessedAuctionRequestHook(ruleParser, datacenter)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHook.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..2f20d25ffa6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHook.java @@ -0,0 +1,114 @@ +package org.prebid.server.hooks.modules.rule.engine.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PbRuleEngineProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "pb-rule-engine-processed-auction-request"; + + private final RuleParser ruleParser; + private final String datacenter; + + public PbRuleEngineProcessedAuctionRequestHook(RuleParser ruleParser, String datacenter) { + this.ruleParser = Objects.requireNonNull(ruleParser); + this.datacenter = Objects.requireNonNull(datacenter); + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final AuctionContext context = invocationContext.auctionContext(); + final String accountId = StringUtils.defaultString(invocationContext.auctionContext().getAccount().getId()); + final ObjectNode accountConfig = invocationContext.accountConfig(); + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + + if (accountConfig == null) { + return succeeded(RuleResult.noAction(bidRequest)); + } + + return ruleParser.parseForAccount(accountId, accountConfig) + .map(PerStageRule::processedAuctionRequestRule) + .map(rule -> rule.process( + bidRequest, RequestRuleContext.of(context, Granularity.Request.instance(), datacenter))) + .flatMap(PbRuleEngineProcessedAuctionRequestHook::succeeded) + .recover(PbRuleEngineProcessedAuctionRequestHook::failure); + } + + private static Future> succeeded(RuleResult result) { + final InvocationResultImpl.InvocationResultImplBuilder resultBuilder = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(toInvocationAction(result.getAction())) + .rejections(toRejections(result.getSeatNonBid())) + .analyticsTags(result.getAnalyticsTags()); + + if (result.isUpdate()) { + resultBuilder.payloadUpdate(initialPayload -> AuctionRequestPayloadImpl.of(result.getValue())); + } + + return Future.succeededFuture(resultBuilder.build()); + } + + private static InvocationAction toInvocationAction(RuleAction ruleAction) { + return switch (ruleAction) { + case NO_ACTION -> InvocationAction.no_action; + case UPDATE -> InvocationAction.update; + case REJECT -> InvocationAction.reject; + }; + } + + private static List toRejections(SeatNonBid seatNonBid) { + return seatNonBid.getNonBid().stream() + .map(nonBid -> (Rejection) ImpRejection.of(nonBid.getImpId(), nonBid.getStatusCode())) + .toList(); + } + + private static Map> toRejections(List seatNonBids) { + return seatNonBids.stream() + .collect(Collectors.groupingBy(SeatNonBid::getSeat, + Collectors.flatMapping( + seatNonBid -> toRejections(seatNonBid).stream(), + Collectors.toList()))); + } + + private static Future> failure(Throwable error) { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .action(InvocationAction.no_invocation) + .message(error.getMessage()) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParserTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParserTest.java new file mode 100644 index 00000000000..fe7a33b5a56 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParserTest.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class AccountConfigParserTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private AccountConfigParser target; + + @Mock(strictness = LENIENT) + private StageConfigParser processedAuctionRequestStageParser; + + @BeforeEach + public void setUp() { + target = new AccountConfigParser(MAPPER, processedAuctionRequestStageParser); + } + + @Test + public void parseShouldReturnNoOpConfigWhenEnabledIsFalse() { + // when and then + assertThat(target.parse(MAPPER.createObjectNode().set("enabled", BooleanNode.getFalse()))).isEqualTo( + PerStageRule.builder() + .timestamp(Instant.EPOCH) + .processedAuctionRequestRule(NoOpRule.create()) + .build()); + } + + @Test + public void parseShouldParseRuleForEachSupportedStage() { + // given + final Rule rule = (Rule) mock(Rule.class); + given(processedAuctionRequestStageParser.parse(any())).willReturn(rule); + + // when and then + assertThat(target.parse(MAPPER.createObjectNode())).isEqualTo( + PerStageRule.builder() + .timestamp(Instant.EPOCH) + .processedAuctionRequestRule(rule) + .build()); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParserTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParserTest.java new file mode 100644 index 00000000000..79875a16a0e --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParserTest.java @@ -0,0 +1,222 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountRuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ModelGroupConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ResultFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.RuleSetConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.SchemaFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.AlternativeActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.CompositeRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.rules.DefaultActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RandomWeightedRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTreeFactory; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedEntry; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.random.RandomGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class StageConfigParserTest { + + private StageConfigParser target; + + @Mock(strictness = LENIENT) + private RandomGenerator randomGenerator; + + @Mock(strictness = LENIENT) + private StageSpecification stageSpecification; + + @Mock(strictness = LENIENT) + private ConditionalRuleFactory conditionalRuleFactory; + + @Mock(strictness = LENIENT) + private SchemaFunction schemaFunction; + + @Mock(strictness = LENIENT) + private ResultFunction resultFunction; + + @Mock(strictness = LENIENT) + private Rule matchingRule; + + @Mock(strictness = LENIENT) + private RuleTreeFactory ruleTreeFactory; + + @BeforeEach + public void setUp() { + target = new StageConfigParser<>( + randomGenerator, Stage.processed_auction_request, stageSpecification, conditionalRuleFactory); + } + + @Test + public void parseShouldReturnNoOpRuleWhenSubrulesForStageAreAbsent() { + // when and then + assertThat(target.parse(AccountConfig.builder().build())).isEqualTo(NoOpRule.create()); + } + + @Test + public void parseShouldReturnNoOpRuleWhenEnabledSubrulesForStageAreAbsent() { + // given + final AccountConfig accountConfig = AccountConfig.builder() + .ruleSets(Collections.singletonList( + RuleSetConfig.builder() + .enabled(false) + .stage(Stage.processed_auction_request) + .build())) + .build(); + + // when and then + assertThat(target.parse(accountConfig)).isEqualTo(NoOpRule.create()); + } + + @Test + public void parseShouldCombineModelGroupRulesUnderSameRuleSetIntoRandomWeightedRule() { + // given + final ModelGroupConfig firstModelGroupConfig = ModelGroupConfig.builder() + .weight(1) + .analyticsKey("analyticsKey1") + .version("version1") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + final ModelGroupConfig secondModelGroupConfig = ModelGroupConfig.builder() + .weight(2) + .analyticsKey("analyticsKey2") + .version("version2") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + final List modelGroupConfigs = Arrays.asList(firstModelGroupConfig, secondModelGroupConfig); + + given(stageSpecification.schemaFunctionByName("function1")).willReturn(schemaFunction); + given(stageSpecification.resultFunctionByName("function2")).willReturn(resultFunction); + given(conditionalRuleFactory.create(any(), any(), any(), any())).willReturn(matchingRule); + + final AccountConfig accountConfig = givenAccountConfig(modelGroupConfigs); + + // when and then + final RandomWeightedRule weightedRule = RandomWeightedRule.of( + randomGenerator, + new WeightedList<>(List.of( + WeightedEntry.of(1, AlternativeActionRule.of(matchingRule, NoOpRule.create())), + WeightedEntry.of(2, AlternativeActionRule.of(matchingRule, NoOpRule.create()))))); + + assertThat(target.parse(accountConfig)).isEqualTo( + CompositeRule.of(Collections.singletonList(weightedRule))); + } + + @Test + public void parseShouldCombineMatchingRuleWithDefaultUnderSameModelGroup() { + // given + final ModelGroupConfig modelGroupConfig = ModelGroupConfig.builder() + .weight(1) + .analyticsKey("analyticsKey") + .version("version") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .defaultAction(List.of(ResultFunctionConfig.of("function3", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + given(stageSpecification.schemaFunctionByName("function1")).willReturn(schemaFunction); + given(stageSpecification.resultFunctionByName("function2")).willReturn(resultFunction); + + final ResultFunction secondResultFunction = mock(ResultFunction.class); + given(stageSpecification.resultFunctionByName("function3")).willReturn(secondResultFunction); + + given(conditionalRuleFactory.create(any(), any(), any(), any())).willReturn(matchingRule); + + final AccountConfig accountConfig = givenAccountConfig(modelGroupConfig); + + // when and then + final DefaultActionRule defaultRule = new DefaultActionRule<>( + Collections.singletonList(ResultFunctionHolder.of("function3", secondResultFunction, null)), + "analyticsKey", + "version"); + + final AlternativeActionRule alternativeRule = AlternativeActionRule.of( + matchingRule, defaultRule); + + final RandomWeightedRule weightedRule = RandomWeightedRule.of( + randomGenerator, new WeightedList<>(List.of(WeightedEntry.of(1, alternativeRule)))); + + assertThat(target.parse(accountConfig)).isEqualTo( + CompositeRule.of(Collections.singletonList(weightedRule))); + } + + @Test + public void parseShouldBuildRuleTreeAndCreateAppropriateMatchingRule() { + // given + final ModelGroupConfig modelGroupConfig = ModelGroupConfig.builder() + .weight(1) + .analyticsKey("analyticsKey") + .version("version") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + given(stageSpecification.schemaFunctionByName("function1")).willReturn(schemaFunction); + given(stageSpecification.resultFunctionByName("function2")).willReturn(resultFunction); + + final Schema schema = Schema.of( + Collections.singletonList(SchemaFunctionHolder.of("function1", schemaFunction, null))); + + given(conditionalRuleFactory.create(eq(schema), any(), eq("analyticsKey"), eq("version"))) + .willReturn(matchingRule); + + final AccountConfig accountConfig = givenAccountConfig(modelGroupConfig); + + // when and then + final AlternativeActionRule alternativeRule = AlternativeActionRule.of( + matchingRule, NoOpRule.create()); + final RandomWeightedRule weightedRule = RandomWeightedRule.of( + randomGenerator, new WeightedList<>(List.of(WeightedEntry.of(1, alternativeRule)))); + + assertThat(target.parse(accountConfig)).isEqualTo( + CompositeRule.of(Collections.singletonList(weightedRule))); + } + + private static AccountConfig givenAccountConfig(ModelGroupConfig modelGroupConfig) { + return givenAccountConfig(Collections.singletonList(modelGroupConfig)); + } + + private static AccountConfig givenAccountConfig(List modelGroupConfigs) { + return AccountConfig.builder() + .ruleSets(Collections.singletonList( + RuleSetConfig.builder() + .stage(Stage.processed_auction_request) + .modelGroups(modelGroupConfigs) + .build())) + .build(); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRuleTest.java new file mode 100644 index 00000000000..0d50d9c2a16 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRuleTest.java @@ -0,0 +1,95 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.util.ListUtil; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class PerImpConditionalRuleTest { + + private PerImpConditionalRule target; + + @Mock(strictness = LENIENT) + private ConditionalRule conditionalRule; + + @BeforeEach + public void setUp() { + target = new PerImpConditionalRule(conditionalRule); + } + + @Test + public void processShouldRunMatchingRulePerImpAndCombineResults() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(Imp.builder().id("1").build(), Imp.builder().id("2").build())) + .build(); + + final RequestRuleContext firstImpContext = RequestRuleContext.of( + AuctionContext.builder().build(), + new Granularity.Imp("1"), + null); + + final BidRequest updatedBidRequest = bidRequest.toBuilder().id("updated").build(); + final List firstActivities = singletonList(ActivityImpl.of("activity1", "success", emptyList())); + final List firstSeatNonBids = singletonList( + SeatNonBid.of("seat1", singletonList(NonBid.of("1", BidRejectionReason.NO_BID)))); + given(conditionalRule.process(bidRequest, firstImpContext)).willReturn( + RuleResult.of( + updatedBidRequest, + RuleAction.UPDATE, + TagsImpl.of(firstActivities), + firstSeatNonBids)); + + final RequestRuleContext secondImpContext = RequestRuleContext.of( + AuctionContext.builder().build(), + new Granularity.Imp("2"), + null); + + final BidRequest resultBidRequest = bidRequest.toBuilder().id("updated2").build(); + final List secondActivities = singletonList(ActivityImpl.of("activity2", "success", emptyList())); + final List secondSeatNonBids = singletonList( + SeatNonBid.of("seat2", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))); + given(conditionalRule.process(updatedBidRequest, secondImpContext)).willReturn( + RuleResult.of( + resultBidRequest, + RuleAction.UPDATE, + TagsImpl.of(secondActivities), + secondSeatNonBids)); + + final RequestRuleContext requestContext = RequestRuleContext.of( + AuctionContext.builder().build(), + Granularity.Request.instance(), + null); + + // when and then + assertThat(target.process(bidRequest, requestContext)).isEqualTo( + RuleResult.of( + resultBidRequest, + RuleAction.UPDATE, + TagsImpl.of(ListUtil.union(firstActivities, secondActivities)), + ListUtil.union(firstSeatNonBids, secondSeatNonBids))); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactoryTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactoryTest.java new file mode 100644 index 00000000000..f0e18b51fae --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactoryTest.java @@ -0,0 +1,63 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ThreadLocalRandom; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class RequestConditionalRuleFactoryTest { + + private final RequestConditionalRuleFactory target = new RequestConditionalRuleFactory(); + + @Mock(strictness = LENIENT) + SchemaFunction schemaFunction; + + @Mock(strictness = LENIENT) + RuleTree> ruleTree; + + @Test + void createShouldReturnConditionalRuleWhenSchemaDoesNotContainPerImpFunctions() { + // given + final Schema schema = Schema.of( + Collections.singletonList(SchemaFunctionHolder.of("function", schemaFunction, null))); + + // when and then + assertThat(target.create(schema, ruleTree, "key", "version")) + .isInstanceOf(ConditionalRule.class); + } + + @Test + void createShouldReturnPerImpConditionalRuleWhenSchemaContainPerImpFunctions() { + // given + final String perImpSchemaFunctionName = takeRandomElement(RequestStageSpecification.PER_IMP_SCHEMA_FUNCTIONS); + final Schema schema = Schema.of( + Collections.singletonList(SchemaFunctionHolder.of(perImpSchemaFunctionName, schemaFunction, null))); + + // when and then + assertThat(target.create(schema, ruleTree, "key", "version")) + .isInstanceOf(PerImpConditionalRule.class); + } + + private static T takeRandomElement(Collection collection) { + return collection + .stream() + .skip(ThreadLocalRandom.current().nextInt(collection.size())) + .findAny() + .orElse(null); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunctionTest.java new file mode 100644 index 00000000000..18b13c413a5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunctionTest.java @@ -0,0 +1,525 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class ExcludeBiddersFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private ExcludeBiddersFunction target; + + @Mock(strictness = LENIENT) + private BidderCatalog bidderCatalog; + + @BeforeEach + void setUp() { + target = new ExcludeBiddersFunction(MAPPER, bidderCatalog); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(null)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Configuration is required, but not provided"); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsInvalid() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("bidders", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class); + } + + @Test + public void validateConfigShouldThrowErrorWhenBiddersFieldIsEmpty() { + // given + final ObjectNode config = MAPPER.createObjectNode(); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("'bidders' field is required"); + } + + @Test + void applyShouldExcludeBiddersSpecifiedInConfigAndEmitSeatNonBidsWithATags() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder1")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("impId")).build(); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder1", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder2")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersSpecifiedInConfigOnlyForSpecifiedImpWhenGranularityIsImp() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder3")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder3")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder3", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder4")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWithoutLiveUidSpecifiedInConfigWhenIfSyncedIdSetToFalse() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext("bidder1"), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder2")) + .ifSyncedId(false) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWitLiveUidSpecifiedInConfigWhenIfSyncedIdSetToTrue() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext("bidder2"), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder2")) + .ifSyncedId(true) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldDiscardImpIfAfterUpdateImpExtHasNoBidders() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder3", "bidder4")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder3").add("bidder4")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder3", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))), + SeatNonBid.of( + "bidder4", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + expectedSeatNonBid)); + } + + @Test + void applyShouldRejectBidRequestIfUpdatedRequestHasNoImps() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo(RuleResult.rejected(givenATags(expectedActivity), expectedSeatNonBid)); + } + + @Test + void applyShouldNotGenerateATagWhenNoAnalyticsKeySpecified() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .modelVersion("modelVersion") + .build(); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder1", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder2")), + RuleAction.UPDATE, + givenATags(), + expectedSeatNonBid)); + } + + private static InfrastructureArguments givenInfrastructureArguments( + RequestRuleContext context) { + + return InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + } + + private static Tags givenATags(Activity... activities) { + return TagsImpl.of(Arrays.asList(activities)); + } + + private AuctionContext givenAuctionContext(String... liveUidBidders) { + final Map uids = Arrays.stream(liveUidBidders) + .collect(Collectors.toMap(Function.identity(), ignored -> UidWithExpiry.live("uid"))); + + final UidsCookie uidsCookie = new UidsCookie( + Uids.builder().uids(uids).build(), new JacksonMapper(MAPPER)); + + Arrays.stream(liveUidBidders).forEach( + bidder -> given(bidderCatalog.cookieFamilyName(bidder)).willReturn(Optional.of(bidder))); + + return AuctionContext.builder() + .uidsCookie(uidsCookie) + .build(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(Arrays.asList(imps)).build(); + } + + private static Imp givenImp(String impId, String... bidders) { + return Imp.builder().id(impId).ext(givenImpExt(bidders)).build(); + } + + private static ObjectNode givenImpExt(String... bidders) { + final ObjectNode biddersNode = MAPPER.createObjectNode(); + final ObjectNode dummyBidderConfigNode = MAPPER.createObjectNode().set("config", TextNode.valueOf("test")); + Arrays.stream(bidders).forEach(bidder -> biddersNode.set(bidder, dummyBidderConfigNode)); + + return MAPPER.createObjectNode() + .set("prebid", MAPPER.createObjectNode().set("bidder", biddersNode)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunctionTest.java new file mode 100644 index 00000000000..f8cc0aeab1a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunctionTest.java @@ -0,0 +1,525 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class IncludeBiddersFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private IncludeBiddersFunction target; + + @Mock(strictness = LENIENT) + private BidderCatalog bidderCatalog; + + @BeforeEach + void setUp() { + target = new IncludeBiddersFunction(MAPPER, bidderCatalog); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(null)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Configuration is required, but not provided"); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsInvalid() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("bidders", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class); + } + + @Test + public void validateConfigShouldThrowErrorWhenBiddersFieldIsEmpty() { + // given + final ObjectNode config = MAPPER.createObjectNode(); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("'bidders' field is required"); + } + + @Test + void applyShouldExcludeBiddersNotSpecifiedInConfigAndEmitSeatNonBidsWithATags() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("impId")).build(); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersNotSpecifiedInConfigOnlyForSpecifiedImpWhenGranularityIsImp() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder3")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder4")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder4", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWithLiveUidOrNotSpecifiedInConfigWhenIfSyncedIdSetToFalse() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder2")) + .ifSyncedId(false) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder1")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder1", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder2")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWithoutLiveUidOrNotSpecifiedInConfigWhenIfSyncedIdSetToTrue() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext("bidder1"), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder1")) + .ifSyncedId(true) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldDiscardImpIfAfterUpdateImpExtHasNoBidders() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1", "bidder2")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder3").add("bidder4")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder3", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))), + SeatNonBid.of( + "bidder4", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + expectedSeatNonBid)); + } + + @Test + void applyShouldRejectBidRequestIfUpdatedRequestHasNoImps() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo(RuleResult.rejected(givenATags(expectedActivity), expectedSeatNonBid)); + } + + @Test + void applyShouldNotGenerateATagWhenNoAnalyticsKeySpecified() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .modelVersion("modelVersion") + .build(); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(), + expectedSeatNonBid)); + } + + private static InfrastructureArguments givenInfrastructureArguments( + RequestRuleContext context) { + + return InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + } + + private static Tags givenATags(Activity... activities) { + return TagsImpl.of(Arrays.asList(activities)); + } + + private AuctionContext givenAuctionContext(String... liveUidBidders) { + final Map uids = Arrays.stream(liveUidBidders) + .collect(Collectors.toMap(Function.identity(), ignored -> UidWithExpiry.live("uid"))); + + final UidsCookie uidsCookie = new UidsCookie( + Uids.builder().uids(uids).build(), new JacksonMapper(MAPPER)); + + Arrays.stream(liveUidBidders).forEach( + bidder -> given(bidderCatalog.cookieFamilyName(bidder)).willReturn(Optional.of(bidder))); + + return AuctionContext.builder() + .uidsCookie(uidsCookie) + .build(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(Arrays.asList(imps)).build(); + } + + private static Imp givenImp(String impId, String... bidders) { + return Imp.builder().id(impId).ext(givenImpExt(bidders)).build(); + } + + private static ObjectNode givenImpExt(String... bidders) { + final ObjectNode biddersNode = MAPPER.createObjectNode(); + final ObjectNode dummyBidderConfigNode = MAPPER.createObjectNode().set("config", TextNode.valueOf("test")); + Arrays.stream(bidders).forEach(bidder -> biddersNode.set(bidder, dummyBidderConfigNode)); + + return MAPPER.createObjectNode() + .set("prebid", MAPPER.createObjectNode().set("bidder", biddersNode)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunctionTest.java new file mode 100644 index 00000000000..92ac210d950 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunctionTest.java @@ -0,0 +1,156 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LogATagFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final LogATagFunction target = new LogATagFunction(MAPPER); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'analyticsValue' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenAnalyticsValueFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'analyticsValue' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenAnalyticsValueFieldIsNotAString() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("analyticsValue", MAPPER.createObjectNode()); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'analyticsValue' is required and has to be a string"); + } + + @Test + public void applyShouldEmitATagForRequestAndNotModifyOperand() { + // given + final RequestRuleContext context = RequestRuleContext.of( + AuctionContext.builder().build(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + + final ObjectNode config = MAPPER.createObjectNode() + .set("analyticsValue", TextNode.valueOf("analyticsValue")); + + final ResultFunctionArguments resultFunctionArguments = + ResultFunctionArguments.of(BidRequest.builder().build(), config, infrastructureArguments); + + // when + final RuleResult result = target.apply(resultFunctionArguments); + + // then + final ObjectNode expectedValues = MAPPER.createObjectNode(); + expectedValues.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedValues.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedValues.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedValues.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedValues.set("resultFunction", TextNode.valueOf("logAtag")); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("*")).build(); + final Result expectedResult = ResultImpl.of("success", expectedValues, expectedAppliedTo); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", "success", Collections.singletonList(expectedResult)); + final Tags expectedTags = TagsImpl.of(Collections.singletonList(expectedActivity)); + + assertThat(result).isEqualTo( + RuleResult.of( + BidRequest.builder().build(), + RuleAction.NO_ACTION, + expectedTags, + Collections.emptyList())); + } + + @Test + public void applyShouldEmitATagForImpAndNotModifyOperand() { + // given + final RequestRuleContext context = RequestRuleContext.of( + AuctionContext.builder().build(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + + final ObjectNode config = MAPPER.createObjectNode() + .set("analyticsValue", TextNode.valueOf("analyticsValue")); + + final ResultFunctionArguments resultFunctionArguments = + ResultFunctionArguments.of(BidRequest.builder().build(), config, infrastructureArguments); + + // when + final RuleResult result = target.apply(resultFunctionArguments); + + // then + final ObjectNode expectedValues = MAPPER.createObjectNode(); + expectedValues.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedValues.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedValues.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedValues.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedValues.set("resultFunction", TextNode.valueOf("logAtag")); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("impId")).build(); + final Result expectedResult = ResultImpl.of("success", expectedValues, expectedAppliedTo); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", "success", Collections.singletonList(expectedResult)); + final Tags expectedTags = TagsImpl.of(Collections.singletonList(expectedActivity)); + + assertThat(result).isEqualTo( + RuleResult.of( + BidRequest.builder().build(), + RuleAction.NO_ACTION, + expectedTags, + Collections.emptyList())); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunctionTest.java new file mode 100644 index 00000000000..f129e12eeed --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunctionTest.java @@ -0,0 +1,121 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AdUnitCodeFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final AdUnitCodeFunction target = new AdUnitCodeFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnGpidWhenPresent() { + // given + final Imp imp = Imp.builder() + .id("impId") + .ext(MAPPER.createObjectNode().put("gpid", "gpid")) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("gpid"); + } + + @Test + public void extractShouldReturnTagidWhenGpidAbsentAndTagidPresent() { + // given + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("tagId"); + } + + @Test + public void extractShouldReturnPbAdSlotWhenGpidAndTagidAreAbsent() { + // given + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + + final Imp imp = Imp.builder().id("impId").ext(ext).build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("pbadslot"); + } + + @Test + public void extractShouldReturnStoredRequestIdWhenGpidAndTagidAndPbAdSlotAreAbsent() { + // given + final ObjectNode prebid = MAPPER.createObjectNode(); + prebid.set("storedrequest", MAPPER.createObjectNode().put("id", "srid")); + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("prebid", prebid); + + final Imp imp = Imp.builder().id("impId").ext(ext).build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("srid"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenAllAdUnitCodeSourcesAreAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), new Granularity.Imp("impId"), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunctionTest.java new file mode 100644 index 00000000000..d3705031a51 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunctionTest.java @@ -0,0 +1,192 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AdUnitCodeInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final AdUnitCodeInFunction target = new AdUnitCodeInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenCodesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenCodesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("codes", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenCodesFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode codesNode = MAPPER.createArrayNode(); + codesNode.add(TextNode.valueOf("test")); + codesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("codes", codesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenGpidPresentInConfiguredCodes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .ext(MAPPER.createObjectNode().put("gpid", "gpid")) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "gpid"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenTagidPresentAndGpidIsAbsentInConfiguredCodes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(MAPPER.createObjectNode().put("gpid", "gpid")) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "tagId"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenPbAdSlotPresentAndGpidAndTagidAreAbsentInConfiguredCodes() { + // given + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + ext.set("gpid", TextNode.valueOf("gpid")); + + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(ext) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "pbadslot"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSridPresentAndGpidAndTagidAndPbAdSlotAreAbsentInConfiguredCodes() { + // given + final ObjectNode prebid = MAPPER.createObjectNode(); + prebid.set("storedrequest", MAPPER.createObjectNode().put("id", "srid")); + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("prebid", prebid); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + ext.set("gpid", TextNode.valueOf("gpid")); + + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(ext) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "srid"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAdUnitCodesDoesNotMatchConfiguredCodes() { + // given + final ObjectNode prebid = MAPPER.createObjectNode(); + prebid.set("storedrequest", MAPPER.createObjectNode().put("id", "srid")); + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("prebid", prebid); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + ext.set("gpid", TextNode.valueOf("gpid")); + + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(ext) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "adUnitCode"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... codes) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithCodes(codes), + RequestRuleContext.of(AuctionContext.builder().build(), new Granularity.Imp("impId"), "datacenter")); + } + + private ObjectNode givenConfigWithCodes(String... codes) { + final ArrayNode codesNode = MAPPER.createArrayNode(); + Arrays.stream(codes).map(TextNode::valueOf).forEach(codesNode::add); + return MAPPER.createObjectNode().set("codes", codesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunctionTest.java new file mode 100644 index 00000000000..8059c530cc8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BundleFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final BundleFunction target = new BundleFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnBundle() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().bundle("bundle").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("bundle"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenBundleIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunctionTest.java new file mode 100644 index 00000000000..4a16a45fae8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunctionTest.java @@ -0,0 +1,112 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BundleInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final BundleInFunction target = new BundleInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenBundlesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenBundlesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("bundles", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenBundlesFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode bundlesNode = MAPPER.createArrayNode(); + bundlesNode.add(TextNode.valueOf("test")); + bundlesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("bundles", bundlesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenBundlePresentInConfiguredBundles() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().bundle("bundle").build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "bundle"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenBundleIsAbsentInConfiguredBundles() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().bundle("bundle").build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedBundle"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... bundles) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithBundles(bundles), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithBundles(String... bundles) { + final ArrayNode bundlesNode = MAPPER.createArrayNode(); + Arrays.stream(bundles).map(TextNode::valueOf).forEach(bundlesNode::add); + return MAPPER.createObjectNode().set("bundles", bundlesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunctionTest.java new file mode 100644 index 00000000000..c310678e178 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunctionTest.java @@ -0,0 +1,88 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ChannelFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final ChannelFunction target = new ChannelFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnChannel() { + // given + final ExtRequest ext = ExtRequest.of( + ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("channel")) + .build()); + + final BidRequest bidRequest = BidRequest.builder().ext(ext).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("channel"); + } + + @Test + public void extractShouldReturnWebWhenChannelIsPbjs() { + // given + final ExtRequest ext = ExtRequest.of( + ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("pbjs")) + .build()); + + final BidRequest bidRequest = BidRequest.builder().ext(ext).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("web"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenChannelIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunctionTest.java new file mode 100644 index 00000000000..7f10492234d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DataCenterFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DataCenterFunction target = new DataCenterFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnDataCenter() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "datacenter"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("datacenter"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenDataCenterIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, null); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String dataCenter) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), dataCenter)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunctionTest.java new file mode 100644 index 00000000000..f5ce0d12a74 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunctionTest.java @@ -0,0 +1,108 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DataCenterInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DataCenterInFunction target = new DataCenterInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("datacenters", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode datacentersNode = MAPPER.createArrayNode(); + datacentersNode.add(TextNode.valueOf("test")); + datacentersNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("datacenters", datacentersNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenDataCenterPresentInConfiguredDatacenters() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "datacenter", "datacenter"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenBundleIsAbsentInConfiguredBundles() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "datacenter", "expectedDatacenter"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String datacenter, + String... expectedDatacenters) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithDataCenters(expectedDatacenters), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), datacenter)); + } + + private ObjectNode givenConfigWithDataCenters(String... dataCenters) { + final ArrayNode dataCentersNode = MAPPER.createArrayNode(); + Arrays.stream(dataCenters).map(TextNode::valueOf).forEach(dataCentersNode::add); + return MAPPER.createObjectNode().set("datacenters", dataCentersNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunctionTest.java new file mode 100644 index 00000000000..635d4701308 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunctionTest.java @@ -0,0 +1,68 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceCountryFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceCountryFunction target = new DeviceCountryFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnDeviceCountry() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().geo(Geo.builder().country("country").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("country"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenChannelIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunctionTest.java new file mode 100644 index 00000000000..ef07231b835 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunctionTest.java @@ -0,0 +1,111 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceCountryInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceCountryInFunction target = new DeviceCountryInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("countries", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode countriesNode = MAPPER.createArrayNode(); + countriesNode.add(TextNode.valueOf("test")); + countriesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("countries", countriesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenDeviceCountryPresentInConfiguredCountries() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().geo(Geo.builder().country("country").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "country"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenDeviceCountryIsAbsentInConfiguredCountries() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedCountry"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... countries) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithCountries(countries), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithCountries(String... countries) { + final ArrayNode countriesNode = MAPPER.createArrayNode(); + Arrays.stream(countries).map(TextNode::valueOf).forEach(countriesNode::add); + return MAPPER.createObjectNode().set("countries", countriesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunctionTest.java new file mode 100644 index 00000000000..9e4fe40ec75 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceTypeFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceTypeFunction target = new DeviceTypeFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnDeviceType() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().devicetype(12345).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("12345"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenChannelIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunctionTest.java new file mode 100644 index 00000000000..0bbfc96d6ca --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunctionTest.java @@ -0,0 +1,110 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceTypeInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceTypeInFunction target = new DeviceTypeInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("types", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArrayOfIntegers() { + // given + final ArrayNode typesNode = MAPPER.createArrayNode(); + typesNode.add(TextNode.valueOf("test")); + typesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("types", typesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void extractShouldReturnTrueWhenDeviceTypePresentInConfiguredTypes() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().devicetype(12345).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 12345); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenDeviceTypeIsAbsentInConfiguredTypes() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 1); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + int... types) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithTypes(types), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithTypes(int... types) { + final ArrayNode typesNode = MAPPER.createArrayNode(); + Arrays.stream(types).mapToObj(IntNode::valueOf).forEach(typesNode::add); + return MAPPER.createObjectNode().set("types", typesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunctionTest.java new file mode 100644 index 00000000000..8183ce51dc2 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunctionTest.java @@ -0,0 +1,136 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DomainFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DomainFunction target = new DomainFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnSitePublisherDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("domain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnAppPublisherDomainWhenSiteHasNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().publisher(Publisher.builder().domain("domain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnDoohPublisherDomainWhenSiteAndAppHaveNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .dooh(Dooh.builder().publisher(Publisher.builder().domain("domain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnSiteDomainWhenPublisherHasNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().domain("domain").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnAppDomainWhenPublisherAndSiteHaveNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().domain("domain").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnDoohDomainWhenPublisherAndSiteAndAppHaveNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .dooh(Dooh.builder().domain("domain").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenDomainIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunctionTest.java new file mode 100644 index 00000000000..20be78ab820 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunctionTest.java @@ -0,0 +1,241 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DomainInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DomainInFunction target = new DomainInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("domains", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode domainsNode = MAPPER.createArrayNode(); + domainsNode.add(TextNode.valueOf("test")); + domainsNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("domains", domainsNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenSitePublisherDomainIsPresentInConfiguredDomains() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("sitePubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "sitePubDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppPublisherDomainIsPresentInConfiguredDomains() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("sitePubDomain").build()).build()) + .app(App.builder().publisher(Publisher.builder().domain("appPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "appPubDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenDoohPublisherDomainIsPresentInConfiguredDomains() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("sitePubDomain").build()).build()) + .app(App.builder().publisher(Publisher.builder().domain("appPubDomain").build()).build()) + .dooh(Dooh.builder().publisher(Publisher.builder().domain("doohPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "doohPubDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSiteDomainIsPresentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(App.builder().publisher(Publisher.builder().domain("appPubDomain").build()).build()) + .dooh(Dooh.builder().publisher(Publisher.builder().domain("doohPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "siteDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppDomainIsPresentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final App app = App.builder() + .publisher(Publisher.builder().domain("appPubDomain").build()) + .domain("appDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(app) + .dooh(Dooh.builder().publisher(Publisher.builder().domain("doohPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "appDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenDoohDomainIsPresentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final App app = App.builder() + .publisher(Publisher.builder().domain("appPubDomain").build()) + .domain("appDomain") + .build(); + + final Dooh dooh = Dooh.builder() + .publisher(Publisher.builder().domain("doohPubDomain").build()) + .domain("doohDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(app) + .dooh(dooh) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "doohDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAllSuppliedDomainsAreAbsentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final App app = App.builder() + .publisher(Publisher.builder().domain("appPubDomain").build()) + .domain("appDomain") + .build(); + + final Dooh dooh = Dooh.builder() + .publisher(Publisher.builder().domain("doohPubDomain").build()) + .domain("doohDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(app) + .dooh(dooh) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... domains) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithDomains(domains), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithDomains(String... domains) { + final ArrayNode domainsNode = MAPPER.createArrayNode(); + Arrays.stream(domains).map(TextNode::valueOf).forEach(domainsNode::add); + return MAPPER.createObjectNode().set("domains", domainsNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunctionTest.java new file mode 100644 index 00000000000..c720f57ff0a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunctionTest.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EidAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final EidAvailableFunction target = new EidAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenEidsArePresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(Eid.builder().build())).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenEidsAreAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + @Test + public void extractShouldReturnFalseWhenEidsListContainsOnlyNulls() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(null)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunctionTest.java new file mode 100644 index 00000000000..f0018bc18f1 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunctionTest.java @@ -0,0 +1,114 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EidInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final EidInFunction target = new EidInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("sources", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode sourcesNode = MAPPER.createArrayNode(); + sourcesNode.add(TextNode.valueOf("test")); + sourcesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("sources", sourcesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenAnyOfUserEidSourcesPresentInConfiguredSources() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(Eid.builder().source("source").build())).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "source"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAllUserEidSourcesAbsentInConfiguredSources() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(Eid.builder().source("source").build())).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedSource"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... sources) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithSources(sources), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithSources(String... sources) { + final ArrayNode sourcesNode = MAPPER.createArrayNode(); + Arrays.stream(sources).map(TextNode::valueOf).forEach(sourcesNode::add); + return MAPPER.createObjectNode().set("sources", sourcesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunctionTest.java new file mode 100644 index 00000000000..dff830c0e2b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunctionTest.java @@ -0,0 +1,160 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class FpdAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final FpdAvailableFunction target = new FpdAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenUserDataPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenUserExtDataPresent() { + // given + final ObjectNode extData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().ext(ExtUser.builder().data(extData).build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSiteContentDataPresent() { + // given + final Site site = Site.builder() + .content(Content.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSiteExtDataPresent() { + // given + final ObjectNode extData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final Site site = Site.builder() + .ext(ExtSite.of(null, extData)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppContentDataPresent() { + // given + final App app = App.builder() + .content(Content.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .app(app) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppExtDataPresent() { + // given + final ObjectNode extData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final App app = App.builder() + .ext(ExtApp.of(null, extData)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .app(app) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenNoFpdAvailable() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunctionTest.java new file mode 100644 index 00000000000..6cff68fbe1d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunctionTest.java @@ -0,0 +1,97 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GppSidAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final GppSidAvailableFunction target = new GppSidAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenPositiveGppSidIsPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(Collections.singletonList(1)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenGppSidIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + @Test + public void extractShouldReturnFalseWhenGppSidListContainsOnlyNulls() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(null)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + @Test + public void extractShouldReturnFalseWhenGppSidListContainsOnlyNonPositiveValues() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(List.of(-1, 0)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunctionTest.java new file mode 100644 index 00000000000..25bbcd00b1d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunctionTest.java @@ -0,0 +1,116 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GppSidInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final GppSidInFunction target = new GppSidInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenSidsFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenSidsFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("sids", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenSidsFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode sidsNode = MAPPER.createArrayNode(); + sidsNode.add(TextNode.valueOf("test")); + sidsNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("sids", sidsNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void extractShouldReturnTrueWhenAnyOfRegsGppSidIsPresentInConfiguredSids() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(Collections.singletonList(1)).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 1); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAllOfRegsGppSidAreAbsentConfiguredSids() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(Collections.singletonList(2)).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 1); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + com.iab.openrtb.request.BidRequest bidRequest, + int... sids) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithSids(sids), + RequestRuleContext.of( + AuctionContext.builder().build(), + Granularity.Request.instance(), + "datacenter")); + } + + private ObjectNode givenConfigWithSids(int... sids) { + final ArrayNode sidsNode = MAPPER.createArrayNode(); + Arrays.stream(sids).mapToObj(IntNode::valueOf).forEach(sidsNode::add); + return MAPPER.createObjectNode().set("sids", sidsNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunctionTest.java new file mode 100644 index 00000000000..50482b7e042 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunctionTest.java @@ -0,0 +1,185 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class MediaTypeInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final MediaTypeInFunction target = new MediaTypeInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("types", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode typesNode = MAPPER.createArrayNode(); + typesNode.add(TextNode.valueOf("test")); + typesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("types", typesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenBannerPresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .banner(Banner.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "banner"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenVideoPresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .video(Video.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "video"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAudioPresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .audio(Audio.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "audio"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenNativePresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .xNative(Native.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "native"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenImpMediaTypeIsAbsentInConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .xNative(Native.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "expectedMediaType"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String impId, + String... types) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithTypes(types), + RequestRuleContext.of(AuctionContext.builder().build(), new Granularity.Imp(impId), "datacenter")); + } + + private ObjectNode givenConfigWithTypes(String... types) { + final ArrayNode typesNode = MAPPER.createArrayNode(); + Arrays.stream(types).map(TextNode::valueOf).forEach(typesNode::add); + return MAPPER.createObjectNode().set("types", typesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunctionTest.java new file mode 100644 index 00000000000..7a8f5f4e2c4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunctionTest.java @@ -0,0 +1,93 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PrebidKeyFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final PrebidKeyFunction target = new PrebidKeyFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'key' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenKeyFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'key' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenKeyFieldIsNotAString() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("key", IntNode.valueOf(1)); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'key' is required and has to be a string"); + } + + @Test + public void extractShouldReturnExtPrebidKvpsValueBySpecifiedKey() { + // given + final ObjectNode extPrebidKvpsNode = MAPPER.createObjectNode().set("key", TextNode.valueOf("value")); + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().kvps(extPrebidKvpsNode).build())) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "key"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("value"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenExtPrebidKvpsValueBySpecifiedKeyIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "key"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String key) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithKey(key), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithKey(String key) { + return MAPPER.createObjectNode().set("key", TextNode.valueOf(key)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunctionTest.java new file mode 100644 index 00000000000..1fd079243f5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TcfInScopeFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final TcfInScopeFunction target = new TcfInScopeFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenTcfInScope() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gdpr(1).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenTcfNotInScope() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunctionTest.java new file mode 100644 index 00000000000..c834345f88c --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunctionTest.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class UserFpdAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final UserFpdAvailableFunction target = new UserFpdAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenNonNullUserDataIsPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenUserExtDataIsPresent() { + // given + final ObjectNode extUserData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().ext(ExtUser.builder().data(extUserData).build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenUserDataAndUserExtDataAreAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRuleTest.java new file mode 100644 index 00000000000..1a7d793dadf --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRuleTest.java @@ -0,0 +1,61 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class AlternativeActionRuleTest { + + private static final RuleResult DELEGATE_RESULT = RuleResult.noAction(new Object()); + private static final RuleResult ALTERNATIVE_RESULT = RuleResult.noAction(new Object()); + + @Mock(strictness = LENIENT) + private Rule delegate; + + @Mock(strictness = LENIENT) + private Rule alternative; + + private AlternativeActionRule target; + + @BeforeEach + public void setUp() { + target = AlternativeActionRule.of(delegate, alternative); + } + + @Test + public void processShouldReturnDelegateResult() { + // given + given(delegate.process(any(), any())).willReturn(DELEGATE_RESULT); + given(alternative.process(any(), any())).willReturn(ALTERNATIVE_RESULT); + + // when + final RuleResult result = target.process(new Object(), new Object()); + + // then + assertThat(result).isEqualTo(DELEGATE_RESULT); + verifyNoInteractions(alternative); + } + + @Test + public void processShouldReturnAlternativeResultWhenNoMatchingRuleException() { + // given + given(delegate.process(any(), any())).willThrow(new NoMatchingRuleException()); + given(alternative.process(any(), any())).willReturn(ALTERNATIVE_RESULT); + + // when + final RuleResult result = target.process(new Object(), new Object()); + + // then + assertThat(result).isEqualTo(ALTERNATIVE_RESULT); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRuleTest.java new file mode 100644 index 00000000000..a53fd44a31b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRuleTest.java @@ -0,0 +1,63 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CompositeRuleTest { + + private static final Object VALUE = new Object(); + + @Test + public void processShouldAccumulateResultFromAllSubrules() { + // given + final Rule firstRule = (Rule) mock(Rule.class); + given(firstRule.process(any(), any())).willAnswer(invocationOnMock -> RuleResult.of( + invocationOnMock.getArgument(0), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("firstActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID)))))); + + final Rule secondRule = (Rule) mock(Rule.class); + given(secondRule.process(any(), any())).willAnswer(invocationOnMock -> RuleResult.of( + invocationOnMock.getArgument(0), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("secondActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))))); + + final CompositeRule target = CompositeRule.of(asList(firstRule, secondRule)); + + // when + final RuleResult result = target.process(VALUE, new Object()); + + // then + final Tags expectedTags = TagsImpl.of( + asList(ActivityImpl.of("firstActivity", "success", emptyList()), + ActivityImpl.of("secondActivity", "success", emptyList()))); + + final List expectedNonBids = List.of( + SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID))), + SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))); + + assertThat(result).isEqualTo(RuleResult.of(VALUE, RuleAction.UPDATE, expectedTags, expectedNonBids)); + + verify(firstRule).process(eq(VALUE), any()); + verify(secondRule).process(eq(VALUE), any()); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleTest.java new file mode 100644 index 00000000000..5b1bd6186d4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleTest.java @@ -0,0 +1,176 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.LookupResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +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 ConditionalRuleTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String ANALYTICS_KEY = "analyticsKey"; + private static final String MODEL_VERSION = "modelVersion"; + + private ConditionalRule target; + + @Mock(strictness = LENIENT) + private Schema schema; + + @Mock(strictness = LENIENT) + private RuleTree> ruleTree; + + @Mock(strictness = LENIENT) + private SchemaFunction firstSchemaFunction; + @Mock(strictness = LENIENT) + private SchemaFunction secondSchemaFunction; + @Mock(strictness = LENIENT) + private ResultFunction firstResultFunction; + @Mock(strictness = LENIENT) + private ResultFunction secondResultFunction; + + @BeforeEach + public void setUp() { + target = new ConditionalRule<>(schema, ruleTree, ANALYTICS_KEY, MODEL_VERSION); + } + + @Test + public void processShouldCorrectlyProcessData() { + // given + final Object value = new Object(); + final Object context = new Object(); + + // two schema functions + final ObjectNode firstSchemaFunctionConfig = MAPPER.createObjectNode(); + final ObjectNode secondSchemaFunctionConfig = MAPPER.createObjectNode(); + final String firstSchemaFunctionName = "firstFunction"; + final String secondSchemaFunctionName = "secondFunction"; + final String firstSchemaFunctionOutput = "firstSchemaOutput"; + final String secondSchemaFunctionOutput = "secondSchemaOutput"; + + given(schema.getFunctions()).willReturn(List.of( + SchemaFunctionHolder.of(firstSchemaFunctionName, firstSchemaFunction, firstSchemaFunctionConfig), + SchemaFunctionHolder.of(secondSchemaFunctionName, secondSchemaFunction, secondSchemaFunctionConfig))); + + given(firstSchemaFunction.extract(eq(SchemaFunctionArguments.of(value, firstSchemaFunctionConfig, context)))) + .willReturn(firstSchemaFunctionOutput); + given(secondSchemaFunction.extract(eq(SchemaFunctionArguments.of(value, secondSchemaFunctionConfig, context)))) + .willReturn(secondSchemaFunctionOutput); + + // two result functions + final String firstRuleActionName = "firstRuleAction"; + final String secondRuleActionName = "secondRuleAction"; + final ObjectNode firstResultFunctionConfig = MAPPER.createObjectNode(); + final ObjectNode secondResultFunctionConfig = MAPPER.createObjectNode(); + final List> resultFunctionHolders = List.of( + ResultFunctionHolder.of(firstRuleActionName, firstResultFunction, firstResultFunctionConfig), + ResultFunctionHolder.of(secondRuleActionName, secondResultFunction, secondResultFunctionConfig)); + + final RuleConfig ruleConfig = RuleConfig.of("ruleCondition", resultFunctionHolders); + + // tree that matches values based on schema functions outputs + final String firstDimensionMatch = "firstMatch"; + final String secondDimensionMatch = "secondMatch"; + given(ruleTree.lookup(eq(List.of(firstSchemaFunctionOutput, secondSchemaFunctionOutput)))) + .willReturn(LookupResult.of(ruleConfig, List.of(firstDimensionMatch, secondDimensionMatch))); + + // infrastructure arguments passed to result functions + final InfrastructureArguments infrastructureArguments = InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults( + Map.of(firstSchemaFunctionName, firstSchemaFunctionOutput, + secondSchemaFunctionName, secondSchemaFunctionOutput)) + .schemaFunctionMatches( + Map.of(firstSchemaFunctionName, firstDimensionMatch, + secondSchemaFunctionName, secondDimensionMatch)) + .ruleFired(ruleConfig.getCondition()) + .analyticsKey(ANALYTICS_KEY) + .modelVersion(MODEL_VERSION) + .build(); + + // result of first result function processing + final Object firstResultFunctionUpdatedValue = new Object(); + final Tags firstTags = TagsImpl.of( + Collections.singletonList( + ActivityImpl.of("firstActivity", "status", Collections.emptyList()))); + final List firstSeatNonBids = Collections.singletonList( + SeatNonBid.of( + "seatA", + Collections.singletonList(NonBid.of("impIdA", BidRejectionReason.NO_BID)))); + + final RuleResult firstResultFunctionOutput = RuleResult.of( + firstResultFunctionUpdatedValue, + RuleAction.UPDATE, + firstTags, + firstSeatNonBids); + + final ResultFunctionArguments firstResultFunctionArgs = ResultFunctionArguments.of( + value, firstResultFunctionConfig, infrastructureArguments); + + given(firstResultFunction.apply(eq(firstResultFunctionArgs))).willReturn(firstResultFunctionOutput); + + // result of second result function processing + final Object secondResultFunctionUpdatedValue = new Object(); + final Tags secondTags = TagsImpl.of( + Collections.singletonList( + ActivityImpl.of("secondActivity", "status", Collections.emptyList()))); + final List secondSeatNonBids = Collections.singletonList( + SeatNonBid.of( + "seatB", + Collections.singletonList(NonBid.of("impIdB", BidRejectionReason.NO_BID)))); + + final RuleResult secondResultFunctionOutput = RuleResult.of( + secondResultFunctionUpdatedValue, + RuleAction.UPDATE, + secondTags, + secondSeatNonBids); + + final ResultFunctionArguments secondResultFunctionArgs = ResultFunctionArguments.of( + firstResultFunctionOutput.getValue(), + secondResultFunctionConfig, + infrastructureArguments); + + given(secondResultFunction.apply(eq(secondResultFunctionArgs))).willReturn(secondResultFunctionOutput); + + // when + final RuleResult result = target.process(value, context); + + // then + assertThat(result).isEqualTo( + RuleResult.of( + secondResultFunctionOutput.getValue(), + RuleAction.UPDATE, + TagsImpl.of(ListUtil.union(firstTags.activities(), secondTags.activities())), + ListUtil.union(firstSeatNonBids, secondSeatNonBids))); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRuleTest.java new file mode 100644 index 00000000000..2a9fa1ceec4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRuleTest.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class DefaultActionRuleTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void processShouldAccumulateResultFromAllRuleActions() { + // given + final Object value = new Object(); + final Object context = new Object(); + + final ObjectNode firstConfig = MAPPER.createObjectNode().set("config", TextNode.valueOf("test")); + final ResultFunction firstFunction = + (ResultFunction) mock(ResultFunction.class); + given(firstFunction.apply(any())).willAnswer(invocationOnMock -> RuleResult.of( + ((ResultFunctionArguments) invocationOnMock.getArgument(0)).getOperand(), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("firstActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID)))))); + + final ObjectNode secondConfig = MAPPER.createObjectNode().set("config", TextNode.valueOf("anotherTest")); + final ResultFunction secondFunction = + (ResultFunction) mock(ResultFunction.class); + given(secondFunction.apply(any())).willAnswer(invocationOnMock -> RuleResult.of( + ((ResultFunctionArguments) invocationOnMock.getArgument(0)).getOperand(), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("secondActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))))); + + final List> actions = List.of( + ResultFunctionHolder.of("firstFunction", firstFunction, firstConfig), + ResultFunctionHolder.of("secondFunction", secondFunction, secondConfig)); + + final DefaultActionRule target = new DefaultActionRule<>( + actions, "analyticsKey", "modelVersion"); + + // when + final RuleResult result = target.process(value, context); + + // then + final Tags expectedTags = TagsImpl.of( + asList(ActivityImpl.of("firstActivity", "success", emptyList()), + ActivityImpl.of("secondActivity", "success", emptyList()))); + + final List expectedNonBids = List.of( + SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID))), + SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))); + + assertThat(result).isEqualTo(RuleResult.of(value, RuleAction.UPDATE, expectedTags, expectedNonBids)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/WeightedRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/WeightedRuleTest.java new file mode 100644 index 00000000000..b9304f40b55 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/WeightedRuleTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; + +import java.util.random.RandomGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class WeightedRuleTest { + + @Test + public void processShouldUtilizeRuleFromWeightedList() { + // given + final WeightedList> ruleList = + (WeightedList>) mock(WeightedList.class); + final RuleResult stub = RuleResult.noAction(new Object()); + given(ruleList.getForSeed(anyInt())).willReturn((left, right) -> stub); + + final RandomGenerator randomGenerator = mock(RandomGenerator.class); + given(randomGenerator.nextDouble()).willReturn(0.5); + + final RandomWeightedRule rule = RandomWeightedRule.of(randomGenerator, ruleList); + + // when + final RuleResult result = rule.process(new Object(), new Object()); + + // then + assertThat(result).isEqualTo(stub); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeTest.java new file mode 100644 index 00000000000..9275612dd2b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class RuleTreeTest { + + @Test + public void getValueShouldReturnExpectedValue() { + // given + final Map> subnodes = Map.of( + "A", + new RuleNode.IntermediateNode<>( + Map.of("B", new RuleNode.LeafNode<>("AB"), "*", new RuleNode.LeafNode<>("AC"))), + "B", + new RuleNode.IntermediateNode<>( + Map.of("B", new RuleNode.LeafNode<>("BB"), "C", new RuleNode.LeafNode<>("BC")))); + + final RuleTree tree = new RuleTree<>(new RuleNode.IntermediateNode<>(subnodes), 2); + + // when and then + assertThat(tree.lookup(asList("A", "B"))).isEqualTo(LookupResult.of("AB", List.of("A", "B"))); + assertThat(tree.lookup(asList("A", "C"))).isEqualTo(LookupResult.of("AC", List.of("A", "*"))); + assertThat(tree.lookup(asList("B", "B"))).isEqualTo(LookupResult.of("BB", List.of("B", "B"))); + assertThat(tree.lookup(asList("B", "C"))).isEqualTo(LookupResult.of("BC", List.of("B", "C"))); + assertThatExceptionOfType(NoMatchingRuleException.class).isThrownBy(() -> tree.lookup(asList("C", "B"))); + assertThatExceptionOfType(NoMatchingRuleException.class).isThrownBy(() -> tree.lookup(singletonList("C"))); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHookTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..97e7d19e18b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHookTest.java @@ -0,0 +1,170 @@ +package org.prebid.server.hooks.modules.rule.engine.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.settings.model.Account; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class PbRuleEngineProcessedAuctionRequestHookTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private PbRuleEngineProcessedAuctionRequestHook target; + + @Mock(strictness = LENIENT) + private RuleParser ruleParser; + + @Mock(strictness = LENIENT) + private AuctionRequestPayload payload; + + @Mock(strictness = LENIENT) + private AuctionInvocationContext invocationContext; + + @Mock(strictness = LENIENT) + private Rule processedAuctionRequestRule; + + @Mock(strictness = LENIENT) + private BidRequest bidRequest; + + @Mock(strictness = LENIENT) + private Tags tags; + + private final AuctionContext auctionContext = AuctionContext.builder().account(Account.empty("1001")).build(); + + @BeforeEach + void setUp() { + target = new PbRuleEngineProcessedAuctionRequestHook(ruleParser, "datacenter"); + + given(invocationContext.auctionContext()).willReturn(auctionContext); + given(payload.bidRequest()).willReturn(bidRequest); + + given(ruleParser.parseForAccount(any(), any())).willReturn( + Future.succeededFuture( + PerStageRule.builder() + .timestamp(Instant.EPOCH) + .processedAuctionRequestRule(processedAuctionRequestRule) + .build())); + } + + @Test + public void callShouldReturnNoActionWhenNoAccountConfigProvided() { + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + } + + @Test + public void callShouldReturnNoActionWhenRuleActionIsNoAction() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willReturn(RuleResult.noAction(bidRequest)); + + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + } + + @Test + public void callShouldReturnPayloadUpdateWhenRuleActionIsUpdate() { + // given + final SeatNonBid seatNonBid = SeatNonBid.of( + "bidder", Collections.singletonList(NonBid.of("impId", BidRejectionReason.NO_BID))); + + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willReturn(RuleResult.of(bidRequest, RuleAction.UPDATE, tags, Collections.singletonList(seatNonBid))); + + // when and then + final Map> rejections = Map.of( + "bidder", List.of(ImpRejection.of("impId", BidRejectionReason.NO_BID))); + + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.rejections()).containsExactlyEntriesOf(rejections); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + assertThat(invocationResult.analyticsTags()).isEqualTo(tags); + }); + } + + @Test + public void callShouldReturnRejectWhenRuleActionIsReject() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willReturn(RuleResult.rejected(tags, Collections.emptyList())); + + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.reject); + assertThat(invocationResult.payloadUpdate()).isNull(); + assertThat(invocationResult.analyticsTags()).isEqualTo(tags); + }); + } + + @Test + public void callShouldReturnFailureOnFailure() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willThrow(PreBidException.class); + + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 228580c9b4e..9913522f89a 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -27,6 +27,7 @@ optable-targeting wurfl-devicedetection live-intent-omni-channel-identity + pb-rule-engine diff --git a/pom.xml b/pom.xml index 5d682c15576..55db6df3ed6 100644 --- a/pom.xml +++ b/pom.xml @@ -149,7 +149,6 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - test com.fasterxml.jackson.dataformat diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index 8aed823f6d1..e44f3e78150 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -1,7 +1,10 @@ package org.prebid.server.auction.model; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Arrays; + /** * The list of the Seat Non Bid codes: * 0 - the bidder is called but declines to bid and doesn't provide a code (for the impression) @@ -117,9 +120,16 @@ public enum BidRejectionReason { this.code = code; } + @JsonCreator + public static BidRejectionReason fromStatusCode(int code) { + return Arrays.stream(values()) + .filter(e -> e.code == code) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Invalid bid rejection reason: " + code)); + } + @JsonValue public int getValue() { return code; } - } diff --git a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java index fc1bcea0611..e34ff41c0c0 100644 --- a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java +++ b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.blackbird.BlackbirdModule; public final class ObjectMapperProvider { @@ -23,6 +24,7 @@ public final class ObjectMapperProvider { .build() .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(new JavaTimeModule()) .registerModule(new BlackbirdModule()) .registerModule(new ZonedDateTimeModule()) .registerModule(new MissingJsonNodeModule()) diff --git a/src/main/java/org/prebid/server/model/UpdateResult.java b/src/main/java/org/prebid/server/model/UpdateResult.java index 3c28be53ebe..a96275d0e42 100644 --- a/src/main/java/org/prebid/server/model/UpdateResult.java +++ b/src/main/java/org/prebid/server/model/UpdateResult.java @@ -2,7 +2,7 @@ import lombok.Value; -@Value +@Value(staticConstructor = "of") public class UpdateResult { boolean updated; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 25dffdc486c..1380e543991 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -198,4 +198,6 @@ public class ExtRequestPrebid { @JsonProperty("alternatebiddercodes") ExtRequestPrebidAlternateBidderCodes alternateBidderCodes; + + ObjectNode kvps; } diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index 2bc06ab7144..9aee1c69c68 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -8,6 +8,7 @@ enum ModuleName { PB_RESPONSE_CORRECTION ("pb-response-correction"), ORTB2_BLOCKING("ortb2-blocking"), PB_REQUEST_CORRECTION('pb-request-correction'), + PB_RULE_ENGINE('pb-rule-engine') @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 247bdea4353..4d9424e1c39 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -11,6 +11,7 @@ enum ModuleHookImplementation { ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response"), PB_REQUEST_CORRECTION_PROCESSED_AUCTION_REQUEST("pb-request-correction-processed-auction-request"), + PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST("pb-rule-engine-processed-auction-request") @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbRulesEngine.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbRulesEngine.groovy new file mode 100644 index 00000000000..321c548394b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbRulesEngine.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.config + +import java.time.ZonedDateTime + +class PbRulesEngine { + + Boolean enabled + Boolean generateRulesFromBidderConfig + ZonedDateTime timestamp + List ruleSets + + static PbRulesEngine createRulesEngineWithRule(Boolean enabled = true) { + new PbRulesEngine().tap { + it.enabled = enabled + it.generateRulesFromBidderConfig = false + it.ruleSets = [RuleSet.createRuleSets()] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 59f640f966c..801178fd4d4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -13,4 +13,5 @@ class PbsModulesConfig { Ortb2BlockingConfig ortb2Blocking PbResponseCorrection pbResponseCorrection PbRequestCorrectionConfig pbRequestCorrection + PbRulesEngine pbRuleEngine } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ResultFunction.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ResultFunction.groovy new file mode 100644 index 00000000000..108e8a00fe0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/ResultFunction.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +enum ResultFunction { + + INCLUDE_BIDDERS("includeBidders"), + EXCLUDE_BIDDER("excludeBidders"), + LOG_A_TAG("logAtag") + + String value + + ResultFunction(String value) { + this.value = value + } + + @Override + @JsonValue + String toString() { + return value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineArguments.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineArguments.groovy new file mode 100644 index 00000000000..20f5855be7b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineArguments.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.config + +import org.prebid.server.functional.model.bidder.BidderName + +class RuleEngineArguments { + + List bidders + Integer seatNonBid + Boolean ifSyncedId + String analyticsValue +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunction.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunction.groovy new file mode 100644 index 00000000000..b67b4dac362 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunction.groovy @@ -0,0 +1,44 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +enum RuleEngineFunction { + + DEVICE_COUNTRY("deviceCountry", null), + DEVICE_COUNTRY_IN("deviceCountryIn", "countries"), + DATA_CENTER("dataCenter", null), + DATA_CENTER_IN("dataCenterIn", "datacenters"), + CHANNEL("channel", null), + EID_AVAILABLE("eidAvailable", null), + EID_IN("eidIn", "sources"), + USER_FPD_AVAILABLE("userFpdAvailable", null), + FPD_AVAILABLE("fpdAvailable", null), + GPP_SID_AVAILABLE("gppSidAvailable", null), + GPP_SID_IN("gppSidIn", "sids"), + TCF_IN_SCOPE("tcfInScope", null), + PERCENT("percent", "pct"), + PREBID_KEY("prebidKey", "key"), + DOMAIN("domain", null), + DOMAIN_IN("domainIn", "domains"), + BUNDLE("bundle", null), + BUNDLE_IN("bundleIn", "bundles"), + MEDIA_TYPE_IN("mediaTypeIn", "types"), + AD_UNIT_CODE("adUnitCode", null), + AD_UNIT_CODE_IN("adUnitCodeIn", "codes"), + DEVICE_TYPE("deviceType", null), + DEVICE_TYPE_IN("deviceTypeIn", "types") + + private String value + private String fieldName + + RuleEngineFunction(String value, String fieldName) { + this.value = value + this.fieldName = fieldName + } + + @JsonValue + @Override + String toString() { + return value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunctionArgs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunctionArgs.groovy new file mode 100644 index 00000000000..a2cb809d0fe --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunctionArgs.groovy @@ -0,0 +1,37 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.util.PBSUtils + +class RuleEngineFunctionArgs { + + List countries + List datacenters + List sources + List sids + @JsonProperty("pct") + Object percent + Object key + List domains + List bundles + List codes + List types + String operator + BigDecimal value + Currency currency + + static RuleEngineFunctionArgs getDefaultFunctionArgs() { + new RuleEngineFunctionArgs().tap { + countries = [PBSUtils.randomString] + datacenters = [PBSUtils.randomString] + sources = [PBSUtils.randomString] + sids = [PBSUtils.randomNumber] + percent = PBSUtils.getRandomNumber(1, 100) + key = PBSUtils.randomString + domains = [PBSUtils.randomString] + bundles = [PBSUtils.randomString] + codes = [PBSUtils.randomString] + types = [PBSUtils.randomString] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefault.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefault.groovy new file mode 100644 index 00000000000..7193bb920dd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefault.groovy @@ -0,0 +1,7 @@ +package org.prebid.server.functional.model.config + +class RuleEngineModelDefault { + + ResultFunction function + RuleEngineModelDefaultArgs args +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefaultArgs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefaultArgs.groovy new file mode 100644 index 00000000000..3d7884926de --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefaultArgs.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.config + +class RuleEngineModelDefaultArgs { + + String analyticsValue +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRule.groovy new file mode 100644 index 00000000000..557fc999e61 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRule.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.config + +import static java.lang.Boolean.TRUE +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult + +class RuleEngineModelRule { + + List conditions + List results + + static RuleEngineModelRule createRuleEngineModelRule() { + new RuleEngineModelRule().tap { + it.conditions = [TRUE as String] + it.results = [createRuleEngineModelRuleWithExcludeResult()] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResult.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResult.groovy new file mode 100644 index 00000000000..cb898171623 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResult.groovy @@ -0,0 +1,38 @@ +package org.prebid.server.functional.model.config + +import org.prebid.server.functional.model.bidder.BidderName + +import static org.prebid.server.functional.model.bidder.BidderName.ACEEX +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.ResultFunction.EXCLUDE_BIDDER +import static org.prebid.server.functional.model.config.ResultFunction.INCLUDE_BIDDERS +import static org.prebid.server.functional.model.config.ResultFunction.LOG_A_TAG + +class RuleEngineModelRuleResult { + + ResultFunction function + RuleEngineModelRuleResultsArgs args + + static RuleEngineModelRuleResult createRuleEngineModelRuleWithIncludeResult(BidderName bidderName = ACEEX, + Boolean ifSyncedId = false) { + new RuleEngineModelRuleResult().tap { + it.function = INCLUDE_BIDDERS + it.args = RuleEngineModelRuleResultsArgs.createRuleEngineModelRuleResultsArgs(bidderName, ifSyncedId) + } + } + + static RuleEngineModelRuleResult createRuleEngineModelRuleWithExcludeResult(BidderName bidderName = OPENX, + Boolean ifSyncedId = false) { + new RuleEngineModelRuleResult().tap { + it.function = EXCLUDE_BIDDER + it.args = RuleEngineModelRuleResultsArgs.createRuleEngineModelRuleResultsArgs(bidderName, ifSyncedId) + } + } + + static RuleEngineModelRuleResult createRuleEngineModelRuleWithLogATagResult() { + new RuleEngineModelRuleResult().tap { + it.function = LOG_A_TAG + it.args = RuleEngineModelRuleResultsArgs.createRuleEngineModelRuleResultsArgsOnlyATag() + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResultsArgs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResultsArgs.groovy new file mode 100644 index 00000000000..c1ec70eb853 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResultsArgs.groovy @@ -0,0 +1,31 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.response.auction.BidRejectionReason +import org.prebid.server.functional.util.PBSUtils + +class RuleEngineModelRuleResultsArgs { + + List bidders + @JsonProperty("seatnonbid") + BidRejectionReason seatNonBid + @JsonProperty("analyticsValue") + String analyticsValue + @JsonProperty("ifSyncedId") + Boolean ifSyncedId + + static RuleEngineModelRuleResultsArgs createRuleEngineModelRuleResultsArgs(BidderName bidderName, Boolean ifSyncedId) { + new RuleEngineModelRuleResultsArgs().tap { + it.bidders = [bidderName] + it.analyticsValue = PBSUtils.randomString + it.ifSyncedId = ifSyncedId + } + } + + static RuleEngineModelRuleResultsArgs createRuleEngineModelRuleResultsArgsOnlyATag() { + new RuleEngineModelRuleResultsArgs().tap { + it.analyticsValue = PBSUtils.randomString + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelSchema.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelSchema.groovy new file mode 100644 index 00000000000..299ad225a89 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelSchema.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY_IN +import static org.prebid.server.functional.model.pricefloors.Country.USA + +@ToString(includeNames = true, ignoreNulls = true) +class RuleEngineModelSchema { + + RuleEngineFunction function + RuleEngineFunctionArgs args + + static RuleEngineModelSchema createDeviceCountryInSchema(List argsCountries = [USA]) { + new RuleEngineModelSchema().tap { + it.function = DEVICE_COUNTRY_IN + it.args = new RuleEngineFunctionArgs(countries: argsCountries) + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleSet.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleSet.groovy new file mode 100644 index 00000000000..32fd4f22828 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleSet.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.config + +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.util.PBSUtils.randomString + +class RuleSet { + + Boolean enabled + Stage stage + String name + String version + List modelGroups + + static RuleSet createRuleSets() { + new RuleSet().tap { + it.enabled = true + it.stage = PROCESSED_AUCTION_REQUEST + it.name = randomString + it.version = randomString + it.modelGroups = [RulesEngineModelGroup.createRulesModuleGroup()] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RulesEngineModelGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RulesEngineModelGroup.groovy new file mode 100644 index 00000000000..f9361465cd4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RulesEngineModelGroup.groovy @@ -0,0 +1,29 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.config.RuleEngineModelRule.createRuleEngineModelRule +import static org.prebid.server.functional.model.config.RuleEngineModelSchema.createDeviceCountryInSchema + +class RulesEngineModelGroup { + + Integer weight + String version + String analyticsKey + List schema + @JsonProperty("default") + List modelDefault + List rules + + static RulesEngineModelGroup createRulesModuleGroup() { + new RulesEngineModelGroup().tap { + it.weight = PBSUtils.getRandomNumber(1, 100) + it.version = PBSUtils.randomString + it.analyticsKey = PBSUtils.randomString + it.schema = [createDeviceCountryInSchema()] + it.modelDefault = [] + it.rules = [createRuleEngineModelRule()] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy index 91a7e54dc37..60ef8fa7884 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy @@ -13,7 +13,7 @@ class Device { UserAgent sua String ip String ipv6 - Integer devicetype + DeviceType devicetype String make String model String os diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DeviceType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DeviceType.groovy new file mode 100644 index 00000000000..b33855723d7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DeviceType.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum DeviceType { + + MOBILE_TABLET_GENERAL(1), + PERSONAL_COMPUTER(2), + CONNECTED_TV(3), + PHONE(4), + TABLET(5), + CONNECTED_DEVICE(6), + SET_TOP_BOX(7), + OOH_DEVICE(8) + + @JsonValue + final Integer value + + DeviceType(Integer value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpUnitCode.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpUnitCode.groovy new file mode 100644 index 00000000000..15f737d317e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpUnitCode.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +enum ImpUnitCode { + + TAG_ID, + GPID, + PB_AD_SLOT, + STORED_REQUEST +} + diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index 3ab6e7a6dbf..23b4e7f87a5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -47,6 +47,8 @@ class Prebid { AlternateBidderCodes alternateBidderCodes @JsonProperty("profiles") List profileNames + @JsonProperty("kvps") + Map keyValuePairs static class Channel { diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy index 7976ae24c2f..a8b737a848a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy @@ -8,6 +8,7 @@ import org.prebid.server.functional.model.request.auction.FetchStatus import org.prebid.server.functional.model.request.auction.Imp import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS_BLOCK @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @@ -20,7 +21,7 @@ class AnalyticResult { static AnalyticResult buildFromImp(Imp imp) { def appliedTo = new AppliedTo(impIds: [imp.id], bidders: [imp.ext.prebid.bidder.configuredBidders.first()]) - def impResult = new ImpResult(status: 'success-block', values: new ModuleValue(richmediaFormat: 'mraid'), appliedTo: appliedTo) + def impResult = new ImpResult(status: SUCCESS_BLOCK, values: new ModuleValue(richmediaFormat: 'mraid'), appliedTo: appliedTo) new AnalyticResult(name: 'reject-richmedia', status: SUCCESS, results: [impResult]) } } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index 2fb7d75bbf7..98b8dafb43e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy @@ -14,6 +14,7 @@ enum BidRejectionReason { REQUEST_BLOCKED_GENERAL(200), REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE(203), REQUEST_BLOCKED_PRIVACY(204), REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy index de157de7775..8740e415f47 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy @@ -4,13 +4,14 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.FetchStatus @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @EqualsAndHashCode class ImpResult { - String status + FetchStatus status ModuleValue values AppliedTo appliedTo } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy index 9a1e9d1b440..c780efaa422 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy @@ -1,16 +1,24 @@ package org.prebid.server.functional.model.response.auction -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) @EqualsAndHashCode class ModuleValue { - ModuleName module - String richmediaFormat + ModuleName module + @JsonProperty("richmedia-format") + String richmediaFormat + String analyticsKey + String analyticsValue + String modelVersion + String conditionFired + String resultFunction + List biddersRemoved + BidRejectionReason seatNonBid + String message } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy index 1bce783d048..fbcfa74683d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum ResponseAction { - UPDATE, NO_ACTION, NO_INVOCATION + UPDATE, NO_ACTION, NO_INVOCATION, REJECT @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy index 16e1fd46459..a30917a18a6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy @@ -3,11 +3,12 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class SeatNonBid { - String seat + BidderName seat List nonBid } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy index f47efd346f8..1e2c67fad6f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy @@ -274,7 +274,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == bidderName.value + assert seatNonBid.seat == bidderName assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -329,7 +329,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == bidderName.value + assert seatNonBid.seat == bidderName assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -787,7 +787,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == UNKNOWN.value + assert seatNonBid.seat == UNKNOWN assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -843,7 +843,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == UNKNOWN.value + assert seatNonBid.seat == UNKNOWN assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1187,7 +1187,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == requestedAllowedBidderCode.value + assert seatNonBid.seat == requestedAllowedBidderCode assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1242,7 +1242,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == requestedAllowedBidderCode.value + assert seatNonBid.seat == requestedAllowedBidderCode assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL @@ -1303,7 +1303,7 @@ class AlternateBidderCodeSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == allowedBidderCodes.value + assert seatNonBid.seat == allowedBidderCodes assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index 0db60125603..c563796496e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1145,7 +1145,7 @@ class BidderParamsSpec extends BaseSpec { ["No match between the configured currencies and bidRequest.cur"] def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == BidderName.GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY @@ -1242,7 +1242,7 @@ class BidderParamsSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == BidderName.ALIAS.value + assert seatNonBid.seat == BidderName.ALIAS assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY diff --git a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy index bd636e9ccea..02adb2ab5e5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.tests import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountBidValidationConfig import org.prebid.server.functional.model.config.AccountConfig @@ -22,6 +23,7 @@ import static org.mockserver.model.HttpStatusCode.OK_200 import static org.mockserver.model.HttpStatusCode.PROCESSING_102 import static org.mockserver.model.HttpStatusCode.SERVICE_UNAVAILABLE_503 import static org.prebid.server.functional.model.AccountStatus.ACTIVE + import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED @@ -57,7 +59,7 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID @@ -83,7 +85,7 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == ERROR_INVALID_BID_RESPONSE } @@ -105,7 +107,7 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == ERROR_BIDDER_UNREACHABLE } @@ -138,7 +140,7 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_SIZE } @@ -169,7 +171,7 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE @@ -216,7 +218,7 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID @@ -277,7 +279,7 @@ class SeatNonBidSpec extends BaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == ERROR_TIMED_OUT } @@ -301,7 +303,7 @@ class SeatNonBidSpec extends BaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy index 73e44cc3232..1582f3b200d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy @@ -219,7 +219,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_ACTION, NO_ACTION] it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() - it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].sort() it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -230,7 +230,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_ACTION] it.analyticsTags.activities.name.flatten() == [AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN] it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] } @@ -280,7 +280,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -291,7 +291,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] } @@ -341,7 +341,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -352,7 +352,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_ACTION] it.analyticsTags.activities.name.flatten() == [AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN] it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] } @@ -395,7 +395,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -433,7 +433,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_ACTION, NO_ACTION] it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() - it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].sort() it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -482,7 +482,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -529,7 +529,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -577,7 +577,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() - it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].sort() it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -626,7 +626,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -796,7 +796,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -859,7 +859,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -923,7 +923,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -934,7 +934,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] } @@ -983,7 +983,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.analyticsTags.activities.name.flatten() == [ORTB2_BLOCKING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SUCCESS_ALLOW].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SUCCESS_ALLOW] it.analyticsTags.activities.results.values.module.flatten().every { it == null } } @@ -1052,7 +1052,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION, NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING, AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } @@ -1121,7 +1121,7 @@ class AbTestingModuleSpec extends ModuleBaseSpec { it.action == [NO_INVOCATION, NO_INVOCATION] it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] - it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 453de43aa3c..f721b93eac0 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -3,12 +3,16 @@ package org.prebid.server.functional.tests.module import org.prebid.server.functional.model.config.Endpoint import org.prebid.server.functional.model.config.ExecutionPlan import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.response.auction.AnalyticResult +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.InvocationResult import org.prebid.server.functional.tests.BaseSpec import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING -import static org.prebid.server.functional.model.ModuleName.PB_REQUEST_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER +import static org.prebid.server.functional.model.ModuleName.PB_REQUEST_CORRECTION +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST @@ -60,4 +64,21 @@ class ModuleBaseSpec extends BaseSpec { ["hooks.${PB_REQUEST_CORRECTION.code}.enabled": "true", "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_REQUEST_CORRECTION, [stage]))] } + + protected static Map getRulesEngineSettings(Endpoint endpoint = OPENRTB2_AUCTION, Stage stage = PROCESSED_AUCTION_REQUEST) { + ["hooks.${PB_RULE_ENGINE.code}.enabled" : "true", + "hooks.${PB_RULE_ENGINE.code}.rule-cache.expire-after-minutes" : "10000", + "hooks.${PB_RULE_ENGINE.code}.rule-cache.max-size" : "20000", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-initial-delay-millis": "10000", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-max-delay-millis" : "10000", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-exponential-factor" : "1.2", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-exponential-jitter" : "1.2", + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RULE_ENGINE, [stage]))] + } + + protected static List getAnalyticResults(BidResponse response) { + response.ext.prebid.modules?.trace?.stages?.first() + ?.outcomes?.first()?.groups?.first() + ?.invocationResults?.first()?.analyticsTags?.activities + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy index 2b2de98750b..a7a97bc8816 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -1521,7 +1521,7 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == ErrorType.GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineAliasSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineAliasSpec.groovy new file mode 100644 index 00000000000..4477e6d9d38 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineAliasSpec.groovy @@ -0,0 +1,262 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.request.auction.Imp + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.OPENX_ALIAS +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithIncludeResult +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineAliasSpec extends RuleEngineBaseSpec { + + def "PBS should leave only hard alias bidder at imps when hard alias bidder include in account config"() { + given: "Bid request with multiply imps bidders" + def bidders = [OPENX, AMX, OPENX_ALIAS, GENERIC] + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, bidders)) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(OPENX_ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.size() == 1 + + and: "Bid response should contain seatBid.seat" + assert bidResponse.seatbid.seat == [OPENX_ALIAS] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved.sort() == MULTI_BID_ADAPTERS.sort() + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 3 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == MULTI_BID_ADAPTERS.sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.flatten().unique() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove hard alias bidder from imps when hard alias bidder excluded in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [OPENX, OPENX_ALIAS, AMX])) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(OPENX_ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == [bidRequest.imp[1].id] + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX_ALIAS + assert seatNonBid.nonBid[0].impId == bidRequest.imp[1].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should leave only soft alias bidder at imps when soft alias bidder include in account config"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [ALIAS, AMX, OPENX])) + ext.prebid.aliases = [(ALIAS.value): GENERIC] + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert bidResponse.seatbid.seat == [ALIAS] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved.sort() == MULTI_BID_ADAPTERS.sort() + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 3 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == MULTI_BID_ADAPTERS.sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.unique().flatten() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove soft alias bidder from imps when soft alias bidder excluded in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [ALIAS, AMX, OPENX])) + ext.prebid.aliases = [(ALIAS.value): GENERIC] + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved == [ALIAS] + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == [bidRequest.imp[1].id] + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == ALIAS + assert seatNonBid.nonBid[0].impId == bidRequest.imp[1].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy new file mode 100644 index 00000000000..d27d44e0fc0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy @@ -0,0 +1,196 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.PbRulesEngine +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.pricefloors.Country +import org.prebid.server.functional.model.request.auction.Amx +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpUnitCode +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Retry + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.OPENX_ALIAS +import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.GPID +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.PB_AD_SLOT +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.STORED_REQUEST +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.TAG_ID +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID + +@Retry //TODO remove in 3.34+ +abstract class RuleEngineBaseSpec extends ModuleBaseSpec { + + protected static final List MULTI_BID_ADAPTERS = [GENERIC, OPENX, AMX].sort() + protected static final String APPLIED_FOR_ALL_IMPS = "*" + protected static final String DEFAULT_CONDITIONS = "default" + protected final static String CALL_METRIC = "modules.module.${PB_RULE_ENGINE.code}.stage.${PROCESSED_AUCTION_REQUEST.metricValue}.hook.${PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST.code}.call" + protected final static String NOOP_METRIC = "modules.module.${PB_RULE_ENGINE.code}.stage.${PROCESSED_AUCTION_REQUEST.metricValue}.hook.${PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST.code}.success.noop" + protected final static String UPDATE_METRIC = "modules.module.${PB_RULE_ENGINE.code}.stage.${PROCESSED_AUCTION_REQUEST.metricValue}.hook.${PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST.code}.success.update" + protected final static Closure INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be an array of strings" + } + + protected final static Closure INVALID_CONFIGURATION_FOR_SINGLE_STRING_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be a string" + } + + protected final static Closure INVALID_CONFIGURATION_FOR_SINGLE_INTEGER_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be an integer" + } + + protected final static Closure INVALID_CONFIGURATION_FOR_INTEGERS_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be an array of integers" + } + + protected static final Map ENABLED_DEBUG_LOG_MODE = ["logging.level.root": "debug"] + protected static final Map OPENX_CONFIG = ["adapters.${OPENX}.enabled" : "true", + "adapters.${OPENX}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + protected static final Map AMX_CONFIG = ["adapters.${AMX}.enabled" : "true", + "adapters.${AMX}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + protected static final Map OPENX_ALIAS_CONFIG = ["adapters.${OPENX}.aliases.${OPENX_ALIAS}.enabled" : "true", + "adapters.${OPENX}.aliases.${OPENX_ALIAS}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + protected static final String CONFIG_DATA_CENTER = PBSUtils.randomString + private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" + private static final Map GENERIC_CONFIG = [ + "adapters.${GENERIC.value}.usersync.redirect.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.redirect.support-cors": false as String, + "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String] + protected static final PrebidServerService pbsServiceWithRulesEngineModule = pbsServiceFactory.getService(GENERIC_CONFIG + + getRulesEngineSettings() + AMX_CONFIG + OPENX_CONFIG + OPENX_ALIAS_CONFIG + ['datacenter-region': CONFIG_DATA_CENTER] + + ENABLED_DEBUG_LOG_MODE) + + protected static BidRequest getDefaultBidRequestWithMultiplyBidders(DistributionChannel distributionChannel = SITE) { + BidRequest.getDefaultBidRequest(distributionChannel).tap { + it.tmax = 5_000 // prevents timeout issues on slow pipelines + it.imp[0].ext.prebid.bidder.amx = new Amx() + it.imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + it.imp[0].ext.prebid.bidder.generic = new Generic() + it.ext.prebid.trace = VERBOSE + it.ext.prebid.returnAllBidStatus = true + } + } + + protected static Imp updateBidderImp(Imp imp, List bidders = MULTI_BID_ADAPTERS) { + imp.ext.prebid.bidder.tap { + openx = bidders.contains(OPENX) ? Openx.defaultOpenx : null + openxAlias = bidders.contains(OPENX_ALIAS) ? Openx.defaultOpenx : null + amx = bidders.contains(AMX) ? new Amx() : null + generic = bidders.contains(GENERIC) ? new Generic() : null + alias = bidders.contains(ALIAS) ? new Generic() : null + } + imp + } + + protected static void updateBidRequestWithGeoCountry(BidRequest bidRequest, Country country = USA) { + bidRequest.device = new Device(geo: new Geo(country: country)) + } + + protected static Account getAccountWithRulesEngine(String accountId, PbRulesEngine ruleEngine) { + def accountHooksConfiguration = new AccountHooksConfiguration(modules: new PbsModulesConfig(pbRuleEngine: ruleEngine)) + new Account(uuid: accountId, config: new AccountConfig(hooks: accountHooksConfiguration)) + } + + protected static BidRequest createBidRequestWithDomains(DistributionChannel type, String domain, boolean usePublisher = true) { + def request = getDefaultBidRequestWithMultiplyBidders(type) + + switch (type) { + case SITE: + if (usePublisher) request.site.publisher.domain = domain + else request.site.domain = domain + break + case APP: + if (usePublisher) request.app.publisher.domain = domain + else request.app.domain = domain + break + case DOOH: + if (usePublisher) request.dooh.publisher.domain = domain + else request.dooh.domain = domain + break + } + request + } + + protected static BidRequest updatePublisherDomain(BidRequest bidRequest, DistributionChannel distributionChannel, String domain) { + switch (distributionChannel) { + case SITE: + bidRequest.site.publisher.domain = domain + break + case APP: + bidRequest.app.publisher.domain = domain + break + case DOOH: + bidRequest.dooh.publisher.domain = domain + break + } + bidRequest + } + + protected static String getImpAdUnitCodeByCode(Imp imp, ImpUnitCode code) { + switch (code) { + case TAG_ID: + return imp.tagId + case GPID: + return imp.ext.gpid + case PB_AD_SLOT: + return imp.ext.data.pbAdSlot + case STORED_REQUEST: + return imp.ext.prebid.storedRequest.id + default: + return null + } + } + + protected static String getImpAdUnitCode(Imp imp) { + [imp?.ext?.gpid, + imp?.tagId, + imp?.ext?.data?.pbAdSlot, + imp?.ext?.prebid?.storedRequest?.id] + .findResult { it } + } + + protected static waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) { + PBSUtils.waitUntil({ + pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + pbsServiceWithRulesEngineModule.isContainLogsByValue("Successfully parsed rule-engine config for account $bidRequest.accountId") + }) + } + + protected static waitUntilFailedParsedAndCacheAccount(bidRequest) { + PBSUtils.waitUntil({ + pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + pbsServiceWithRulesEngineModule.isContainLogsByValue("Failed to parse rule-engine config for account $bidRequest.accountId") + }) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy new file mode 100644 index 00000000000..5bea040218e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy @@ -0,0 +1,1146 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.pricefloors.MediaType +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpExtContextData +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.Publisher +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.ChannelType.WEB +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.AD_UNIT_CODE +import static org.prebid.server.functional.model.config.RuleEngineFunction.AD_UNIT_CODE_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.BUNDLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.BUNDLE_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.CHANNEL +import static org.prebid.server.functional.model.config.RuleEngineFunction.DOMAIN +import static org.prebid.server.functional.model.config.RuleEngineFunction.DOMAIN_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.MEDIA_TYPE_IN +import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.GPID +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.PB_AD_SLOT +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.STORED_REQUEST +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.TAG_ID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineContextSpec extends RuleEngineBaseSpec { + + def "PBS should exclude bidder when channel match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: CHANNEL)] + rules[0].conditions = [WEB.value] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when channel not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: CHANNEL)] + rules[0].conditions = [PBSUtils.getRandomEnum(ChannelType, [WEB]).value] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when domain match with condition"() { + given: "Default bid request with multiply bidder" + def randomDomain = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders(distributionChannel).tap { + updatePublisherDomain(it, distributionChannel, randomDomain) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DOMAIN)] + rules[0].conditions = [randomDomain] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + distributionChannel << DistributionChannel.values() + } + + def "PBS shouldn't exclude bidder when domain not match with condition"() { + given: "Default bid request with random domain" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(distributionCahannel).tap { + updatePublisherDomain(it, distributionCahannel, PBSUtils.randomString) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DOMAIN)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + distributionCahannel << DistributionChannel.values() + } + + def "PBS should reject processing the rule engine when the domainIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiplyB bidders" + def bidRequest = bidRequestWithDomaint + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DOMAIN_IN + it.args = new RuleEngineFunctionArgs(domains: [PBSUtils.randomNumber]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, DOMAIN_IN)) + + where: + bidRequestWithDomaint << [ + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.domain = PBSUtils.randomString + }] + } + + def "PBS should exclude bidder when domainIn match with condition"() { + given: "Default bid request with multiply bidder" + def randomDomain = PBSUtils.randomString + def bidRequest = createBidRequestWithDomains(type, randomDomain, usePublisher) + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DOMAIN_IN + it.args = new RuleEngineFunctionArgs(domains: [PBSUtils.randomString, randomDomain]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + type | usePublisher + SITE | true + SITE | false + APP | true + APP | false + DOOH | true + DOOH | false + } + + def "PBS shouldn't exclude bidder when domainIn not match with condition"() { + given: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DOMAIN_IN + it.args = new RuleEngineFunctionArgs(domains: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + bidRequest << [ + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.domain = PBSUtils.randomString + }] + } + + def "PBS should exclude bidder when bundle match with condition"() { + given: "Default bid request with multiply bidder" + def bundle = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = bundle + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: BUNDLE)] + rules[0].conditions = [bundle] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when bundle not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = PBSUtils.randomString + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: BUNDLE)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the bundleIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = PBSUtils.randomString + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = BUNDLE_IN + it.args = new RuleEngineFunctionArgs(bundles: [PBSUtils.randomNumber]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, BUNDLE_IN)) + } + + def "PBS should exclude bidder when bundleIn match with condition"() { + given: "Default bid request with multiply bidders" + def bundle = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = bundle + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = BUNDLE_IN + it.args = new RuleEngineFunctionArgs(bundles: [bundle]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when bundleIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = PBSUtils.randomString + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = BUNDLE_IN + it.args = new RuleEngineFunctionArgs(bundles: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Save account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the mediaTypeIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = MEDIA_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [mediaTypeInArgs]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + then: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, MEDIA_TYPE_IN)) + + where: + mediaTypeInArgs << [null, PBSUtils.randomNumber] + } + + def "PBS should exclude bidder when mediaTypeIn match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Setup bidder response" + def bidderResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidderResponse) + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = MEDIA_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [BANNER.value]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when mediaTypeIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = MEDIA_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.getRandomEnum(MediaType, [BANNER])]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when adUnitCode match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + imp[0].ext.gpid = gpid + imp[0].tagId = tagId + imp[0].ext.data = new ImpExtContextData(pbAdSlot: pbAdSlot) + imp[0].ext.prebid.storedRequest = prebidStoredRequest + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: AD_UNIT_CODE)] + rules[0].conditions = [getImpAdUnitCode(bidRequest.imp[0])] + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + gpid | tagId | pbAdSlot | prebidStoredRequest + PBSUtils.getRandomString() | null | null | null + null | PBSUtils.getRandomString() | null | null + null | null | PBSUtils.getRandomString() | null + null | null | null | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + PBSUtils.getRandomString() | PBSUtils.getRandomString() | PBSUtils.getRandomString() | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + null | PBSUtils.getRandomString() | PBSUtils.getRandomString() | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + null | null | PBSUtils.getRandomString() | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + null | null | null | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + } + + def "PBS shouldn't exclude bidder when adUnitCode not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: AD_UNIT_CODE)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the adUnitCodeIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + imp[0].tagId = PBSUtils.randomString + imp[0].ext.gpid = PBSUtils.randomString + imp[0].ext.data = new ImpExtContextData(pbAdSlot: PBSUtils.randomString) + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.getDefaultImpression() + } + storedImpDao.save(storedImp) + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = AD_UNIT_CODE_IN + it.args = new RuleEngineFunctionArgs(codes: [arguments]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, AD_UNIT_CODE_IN)) + + where: + arguments << [PBSUtils.randomBoolean, PBSUtils.randomNumber] + } + + def "PBS should exclude bidder when adUnitCodeIn match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + imp[0].tagId = PBSUtils.randomString + imp[0].ext.gpid = PBSUtils.randomString + imp[0].ext.data = new ImpExtContextData(pbAdSlot: PBSUtils.randomString) + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = AD_UNIT_CODE_IN + it.args = new RuleEngineFunctionArgs(codes: [PBSUtils.randomString, + getImpAdUnitCodeByCode(bidRequest.imp[0], impUnitCode)]) + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + impUnitCode << [TAG_ID, GPID, PB_AD_SLOT, STORED_REQUEST] + } + + def "PBS shouldn't exclude bidder when adUnitCodeIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = AD_UNIT_CODE_IN + it.args = new RuleEngineFunctionArgs(codes: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineCoreSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineCoreSpec.groovy new file mode 100644 index 00000000000..8e9de1807c9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineCoreSpec.groovy @@ -0,0 +1,870 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineModelDefault +import org.prebid.server.functional.model.config.RuleEngineModelDefaultArgs +import org.prebid.server.functional.model.config.RuleSet +import org.prebid.server.functional.model.config.RulesEngineModelGroup +import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.ResultFunction.LOG_A_TAG +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithIncludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithLogATagResult +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_NO_BID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineCoreSpec extends RuleEngineBaseSpec { + + def "PBS should remove bidder and not update analytics when bidder matched with conditions and without analytics key"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets without analytics value" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].analyticsKey = null + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + and: "Flush metric" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBs should populate call and update metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC] == 1 + assert metrics[UPDATE_METRIC] == 1 + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should remove bidder from imps and use default 203 value for seatNonBid when seatNonBid null and exclude bidder in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression)) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + it.ruleSets[0].modelGroups[0].rules[0].results[0].args.seatNonBid = null + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid.impId.sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove bidder from imps and not update seatNonBid when returnAllBidStatus disabled and exclude bidder in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression)) + updateBidRequestWithGeoCountry(it) + ext.prebid.tap { + returnAllBidStatus = false + trace = VERBOSE + } + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + it.ruleSets[0].modelGroups[0].rules[0].results[0].args.seatNonBid = ERROR_NO_BID + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Response shouldn't populate seatNon bid with code 203" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == ERROR_NO_BID + it.appliedTo.impIds == bidRequest.imp.id + } + } + + def "PBS shouldn't include unknown bidder when unknown bidder specified in result account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(UNKNOWN)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.size() == 0 + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + } + + def "PBS shouldn't exclude unknown bidder when unknown bidder specified in result account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(UNKNOWN)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + } + + def "PBS should include one bidder and update analytics when multiple bidders specified and one included in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(OPENX)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat == [OPENX] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == [GENERIC, AMX].sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 2 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == [GENERIC, AMX].sort() + assert seatNonBid.nonBid.impId.flatten() == [bidRequest.imp[0].id, bidRequest.imp[0].id] + assert seatNonBid.nonBid.statusCode.flatten() == + [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove bidder by device geo from imps when bidder excluded in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(Imp.defaultImpression) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seatBids" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid.impId.sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should leave only include bidder at imps when bidder include in account config"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [OPENX, AMX])) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat == [GENERIC] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved == [AMX, OPENX] + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 2 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == [AMX, OPENX].sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.flatten().unique() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should only logATag when present only function log a tag"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [OPENX, AMX])) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithLogATagResult()] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + def impResult = result.results[0] + verifyAll(impResult) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + + it.appliedTo.impIds == [APPLIED_FOR_ALL_IMPS] + } + + verifyAll(impResult) { + !it.values.biddersRemoved + !it.values.seatNonBid + } + } + + def "PBS should remove bidder and update analytics when first rule sets disabled and second enabled in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets.first.enabled = false + it.ruleSets.add(RuleSet.createRuleSets()) + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[1].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should skip rule set and take next one when rule sets not a processed auction request"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules engine and several rule sets" + def firstResults = [createRuleEngineModelRuleWithExcludeResult(GENERIC), + createRuleEngineModelRuleWithExcludeResult(AMX), + createRuleEngineModelRuleWithExcludeResult(OPENX)] + def secondResult = [createRuleEngineModelRuleWithExcludeResult(AMX)] + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = firstResults + it.ruleSets[0].stage = stage as Stage + it.ruleSets.add(RuleSet.createRuleSets()) + it.ruleSets[1].modelGroups[0].rules[0].results = secondResult + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, OPENX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[1].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == AMX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + stage << Stage.values() - PROCESSED_AUCTION_REQUEST + } + + def "PBS should take rule with higher weight and remove bidder when two model group with different weight"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with few model group" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + it.weight = 1 + it.rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC)] + } + it.ruleSets[0].modelGroups.add(RulesEngineModelGroup.createRulesModuleGroup()) + it.ruleSets[0].modelGroups[1].tap { + it.weight = 100 + it.rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + } + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[1] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't log the default model group and should modify response when other rule fire"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with default model" + def analyticsValue = PBSUtils.randomString + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = [new RuleEngineModelDefault( + function: LOG_A_TAG, + args: new RuleEngineModelDefaultArgs(analyticsValue: analyticsValue))] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should log the default model group and shouldn't modify response when other rules not fire"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it, BULGARIA) + } + + and: "Account with default model" + def analyticsValue = PBSUtils.randomString + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = [new RuleEngineModelDefault( + function: LOG_A_TAG, + args: new RuleEngineModelDefaultArgs(analyticsValue: analyticsValue))] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + def impResult = result.results[0] + verifyAll(impResult) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == analyticsValue + it.values.resultFunction == LOG_A_TAG.value + it.values.conditionFired == DEFAULT_CONDITIONS + it.appliedTo.impIds == [APPLIED_FOR_ALL_IMPS] + } + + and: "Analytics imp result shouldn't contain remove info" + verifyAll(impResult) { + !it.values.biddersRemoved + !it.values.seatNonBid + } + } + + def "PBS shouldn't log the default model group and modify response when rules fire"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with default model" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = [new RuleEngineModelDefault( + function: LOG_A_TAG, + args: new RuleEngineModelDefaultArgs(analyticsValue: PBSUtils.randomString))] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain two seat" + assert bidResponse.seatbid.size() == 2 + + and: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy new file mode 100644 index 00000000000..229e54a0ff0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy @@ -0,0 +1,435 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.model.pricefloors.Country +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.DeviceType +import org.prebid.server.functional.util.PBSUtils +import spock.lang.RepeatUntilFailure + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_TYPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_TYPE_IN +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineDeviceSpec extends RuleEngineBaseSpec { + + def "PBS should exclude bidder when deviceCountry match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DEVICE_COUNTRY)] + rules[0].conditions = [USA.toString()] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when deviceCountry not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema[0].function = DEVICE_COUNTRY + rules[0].conditions = [PBSUtils.getRandomEnum(Country, [USA]).toString()] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when deviceType match with condition"() { + given: "Default bid request with multiply bidders" + def deviceType = PBSUtils.getRandomEnum(DeviceType) + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: deviceType) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DEVICE_TYPE)] + rules[0].conditions = [deviceType.value as String] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when deviceType not match with condition"() { + given: "Default bid request with multiply bidders" + def requestDeviceType = PBSUtils.getRandomEnum(DeviceType) + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: requestDeviceType) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DEVICE_TYPE)] + rules[0].conditions = [PBSUtils.getRandomEnum(DeviceType, [requestDeviceType]).value as String] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the deviceCountryIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: PBSUtils.getRandomEnum(DeviceType)) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_COUNTRY_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.randomString]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, DEVICE_COUNTRY_IN)) + } + + def "PBS should reject processing the rule engine when the deviceTypeIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: PBSUtils.getRandomEnum(DeviceType)) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.getRandomString()]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_INTEGERS_LOG_WARNING(bidRequest.accountId, DEVICE_TYPE_IN)) + } + + def "PBS should exclude bidder when deviceTypeIn match with condition"() { + given: "Default bid request with multiply bidders" + def deviceType = PBSUtils.getRandomEnum(DeviceType) + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: deviceType) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [deviceType.value]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when deviceTypeIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: PBSUtils.getRandomEnum(DeviceType)) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.getRandomEnum(DeviceType).value as String]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineInfrastructureSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineInfrastructureSpec.groovy new file mode 100644 index 00000000000..f710ba39f93 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineInfrastructureSpec.groovy @@ -0,0 +1,264 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.DATA_CENTER +import static org.prebid.server.functional.model.config.RuleEngineFunction.DATA_CENTER_IN +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineInfrastructureSpec extends RuleEngineBaseSpec { + + def "PBS should reject processing rule engine when dataCenterIn schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DATA_CENTER_IN + it.args = new RuleEngineFunctionArgs(countries: [CONFIG_DATA_CENTER]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, DATA_CENTER_IN)) + } + + def "PBS should exclude bidder when dataCenterIn match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DATA_CENTER_IN + it.args = new RuleEngineFunctionArgs(datacenters: [CONFIG_DATA_CENTER]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when dataCentersIn not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DATA_CENTER_IN + it.args = new RuleEngineFunctionArgs(datacenters: [PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when dataCenter match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DATA_CENTER)] + rules[0].conditions = [CONFIG_DATA_CENTER] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when dataCenter not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DATA_CENTER)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy new file mode 100644 index 00000000000..c4f207544b5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy @@ -0,0 +1,915 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.model.request.GppSectionId +import org.prebid.server.functional.model.request.auction.AppExt +import org.prebid.server.functional.model.request.auction.AppExtData +import org.prebid.server.functional.model.request.auction.Content +import org.prebid.server.functional.model.request.auction.Data +import org.prebid.server.functional.model.request.auction.Eid +import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.SiteExt +import org.prebid.server.functional.model.request.auction.SiteExtData +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.util.PBSUtils +import org.prebid.server.functional.util.privacy.TcfConsent + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.EID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.EID_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.FPD_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.TCF_IN_SCOPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.USER_FPD_AVAILABLE +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS + +class RuleEnginePrivacySpec extends RuleEngineBaseSpec { + + def "PBS should exclude bidder when eidAvailable match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: [Eid.getDefaultEid()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: EID_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when eidAvailable not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: eids) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema[0].function = EID_AVAILABLE + rules[0].conditions = ["TRUE"] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + eids << [null, []] + } + + def "PBS should reject processing rule engine when eidIn schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = EID_IN + it.args = new RuleEngineFunctionArgs(sources: [PBSUtils.randomNumber]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, EID_IN)) + } + + def "PBS should exclude bidder when eidIn match with condition"() { + given: "Bid request with multiply bidders" + def eid = Eid.getDefaultEid() + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: [eid]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = EID_IN + it.args = new RuleEngineFunctionArgs(sources: [PBSUtils.randomString, eid.source, PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when eidIn not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: [Eid.getDefaultEid()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = EID_IN + it.args = new RuleEngineFunctionArgs(sources: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when userFpdAvailable match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = requestedUfpUser + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: USER_FPD_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + requestedUfpUser << [new User(data: [Data.defaultData], ext: new UserExt(data: UserExtData.FPDUserExtData)), + new User(ext: new UserExt(data: UserExtData.FPDUserExtData)), + new User(data: [Data.defaultData])] + } + + def "PBS shouldn't exclude bidder when userFpdAvailable not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = requestedUfpUser + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: USER_FPD_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + requestedUfpUser << [new User(data: null), new User(data: [null]), + new User(ext: new UserExt(data: null)), + new User(data: null, ext: new UserExt(data: null)) + ] + } + + def "PBS should exclude bidder when fpdAvailable match with condition"() { + given: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: FPD_AVAILABLE)] + } + + and: "Account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + bidRequest << [ + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(data: [Data.defaultData]) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(ext: new UserExt(data: UserExtData.FPDUserExtData)) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.content = new Content(data: [Data.defaultData]) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.ext = new SiteExt(data: SiteExtData.FPDSiteExtData) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.content = new Content(data: [Data.defaultData]) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.ext = new AppExt(data: new AppExtData(language: PBSUtils.randomString)) + } + ] + } + + def "PBS shouldn't exclude bidder when fpdAvailable not match with condition"() { + given: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: FPD_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + bidRequest << [ + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(data: null, ext: new UserExt(data: null)) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(ext: new UserExt(data: null)) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.content = new Content(data: [null]) + site.ext = new SiteExt(data: null) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.ext = new SiteExt(data: null) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.content = new Content(data: [null]) + app.ext = new AppExt(data: null) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.ext = new AppExt(data: null) + } + ] + } + + def "PBS should exclude bidder when gppSidAvailable match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: [PBSUtils.getRandomEnum(GppSectionId).getIntValue()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: GPP_SID_AVAILABLE)] + } + + and: "Account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when gppSidAvailable not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: gppSid) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: GPP_SID_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + gppSid << [[PBSUtils.randomNegativeNumber], null] + } + + def "PBS should reject processing rule engine when gppSidIn schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gdpr: 0, gppSid: [PBSUtils.getRandomEnum(GppSectionId, [GppSectionId.TCF_EU_V2]).getIntValue()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = GPP_SID_IN + it.args = new RuleEngineFunctionArgs(sids: [PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_INTEGERS_LOG_WARNING(bidRequest.accountId, GPP_SID_IN)) + } + + def "PBS should exclude bidder when gppSidIn match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: [gppSectionId.getIntValue()]) + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = GPP_SID_IN + it.args = new RuleEngineFunctionArgs(sids: [gppSectionId]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved == groups.rules.first.results.first.args.bidders + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + gppSectionId << GppSectionId.values() - GppSectionId.TCF_EU_V2 + } + + def "PBS shouldn't exclude bidder when gppSidIn not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: [gppSectionId.getIntValue()]) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = GPP_SID_IN + it.args = new RuleEngineFunctionArgs(sids: [PBSUtils.getRandomEnum(GppSectionId, [gppSectionId]).getIntValue()]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + gppSectionId << GppSectionId.values() - GppSectionId.TCF_EU_V2 + } + + def "PBS should exclude bidder when tcfInScope match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gdpr: gdpr) + user = new User(ext: new UserExt(consent: new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build())) + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: TCF_IN_SCOPE)] + rules[0].conditions = [condition] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved == groups.rules.first.results.first.args.bidders + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + gdpr | condition + 1 | 'true' + 0 | 'false' + } + + def "PBS shouldn't exclude bidder when tcfInScope not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gdpr: gdpr) + user = new User(ext: new UserExt(consent: new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build())) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: TCF_IN_SCOPE)] + rules[0].conditions = [condition] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + gdpr | condition + 0 | 'true' + 1 | 'false' + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy new file mode 100644 index 00000000000..cce9631e1b9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy @@ -0,0 +1,321 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.PERCENT +import static org.prebid.server.functional.model.config.RuleEngineFunction.PREBID_KEY +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineSpecialSpec extends RuleEngineBaseSpec { + + def "PBS should reject processing rule engine when percent schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PERCENT + it.args = new RuleEngineFunctionArgs(percent: PBSUtils.randomString) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result should contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_SINGLE_INTEGER_LOG_WARNING(bidRequest.accountId, PERCENT)) + } + + def "PBS should exclude bidder when percent match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PERCENT + it.args = new RuleEngineFunctionArgs(percent: PBSUtils.getRandomNumber(100)) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when percent less than zero"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PERCENT + it.args = new RuleEngineFunctionArgs(percent: percent) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + percent << [0, PBSUtils.randomNegativeNumber] + } + + def "PBS should reject processing the rule engine when the prebidKey schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PREBID_KEY + it.args = new RuleEngineFunctionArgs(key: PBSUtils.randomNumber) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response should not contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_SINGLE_STRING_LOG_WARNING(bidRequest.accountId, PREBID_KEY)) + } + + def "PBS should exclude bidder when prebidKey match with condition"() { + given: "Default bid request with multiply bidder" + def keyField = "key" + def keyString = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + ext.prebid.keyValuePairs = [(keyString): keyField] + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PREBID_KEY + it.args = new RuleEngineFunctionArgs((keyField): keyString) + } + it.ruleSets[0].modelGroups[0].rules[0].conditions = [keyField] + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when prebidKey not match with condition"() { + given: "Default bid request with multiply bidder" + def key = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + ext.prebid.keyValuePairs = [(key): PBSUtils.randomString] + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PREBID_KEY + it.args = new RuleEngineFunctionArgs(key: key) + } + it.ruleSets[0].modelGroups[0].rules[0].conditions = [PBSUtils.randomString] + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSyncSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSyncSpec.groovy new file mode 100644 index 00000000000..881a8211ca4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSyncSpec.groovy @@ -0,0 +1,298 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.util.HttpUtil + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithIncludeResult +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineSyncSpec extends RuleEngineBaseSpec { + + def "PBS should remove bidder from imps when bidder has ID in the uids cookie and bidder excluded and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX] + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about module exclude" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should contain seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't remove bidder from imps when bidder has ID in the uids cookie and bidder excluded and ifSyncedId=false in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC, false)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS shouldn't remove bidder from imps when bidder hasn't ID in the uids cookie and bidder excluded and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should remove requested bidders at imps when bidder has ID in the uids cookie and bidder include and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response shouldn't contain seat" + assert bidResponse.seatbid.seat == [GENERIC] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == [AMX, OPENX].sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should contain seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 2 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == [OPENX, AMX].sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.flatten().unique() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS shouldn't include bidder at imps when bidder has ID in the uids cookie and bidder include and ifSyncedId=false in account config"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC, false)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response shouldn't contain seat" + assert !bidResponse.seatbid.seat + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == [OPENX, AMX, GENERIC].sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response shouldn't contain seatNon bid with code 203" + assert !bidResponse.ext.seatnonbid + } + + def "PBS should leave request bidder at imps when bidder hasn't ID in the uids cookie and bidder excluded and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy new file mode 100644 index 00000000000..dbf7395c1f0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy @@ -0,0 +1,437 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.AD_UNIT_CODE +import static org.prebid.server.functional.model.config.RuleEngineFunction.BUNDLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.CHANNEL +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_TYPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.DOMAIN +import static org.prebid.server.functional.model.config.RuleEngineFunction.EID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.FPD_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.TCF_IN_SCOPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.USER_FPD_AVAILABLE +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA + +class RuleEngineValidationSpec extends RuleEngineBaseSpec { + + def "PBS shouldn't remove bidder when rule engine not fully configured in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRulesEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + and: "Flush metrics" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBs should populate call and noop metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC] == 1 + assert metrics[NOOP_METRIC] == 1 + + and: "PBs should populate update metrics" + assert !metrics[UPDATE_METRIC] + + where: + pbRulesEngine << [ + createRulesEngineWithRule().tap { it.ruleSets = [] }, + createRulesEngineWithRule().tap { it.ruleSets[0].stage = null }, + createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].schema = [] }, + createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules = [] }, + createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules[0].results = [] } + ] + } + + def "PBS shouldn't remove bidder when rule engine not fully configured in account without rule conditions"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def pbRuleEngine = createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules[0].conditions = [] } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + and: "Flush metrics" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBs should populate noop metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[NOOP_METRIC] == 1 + } + + def "PBS shouldn't remove bidder and emit a warning when args rule engine not fully configured in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def pbRuleEngine = createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules[0].results[0].args = null } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + and: "Flush metrics" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should populate failer metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[NOOP_METRIC] == 1 + } + + def "PBS shouldn't remove bidder and emit a warning when model group rule engine not fully configured in account"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def pbRulesEngine = createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups = [] } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRulesEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should emit failed logs" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, "Failed to parse rule-engine config for account $bidRequest.accountId:" + + " Weighted list cannot be empty") + } + + def "PBS shouldn't log default model when rule does not fired and empty model default"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it, BULGARIA) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = null + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS shouldn't remove bidder when rule engine disabled or absent in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + where: + pbRuleEngine << [createRulesEngineWithRule(false), null] + } + + def "PBS shouldn't remove bidder when rule sets disabled in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with disabled rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets.first.enabled = false + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + } + + def "PBS shouldn't remove any bidder without cache account request"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule() + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBs should emit failed logs" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, "Parsing rule for account $bidRequest.accountId").size() == 1 + } + + def "PBS shouldn't take rule with higher weight and not remove bidder when weight negative or zero"() { + given: "Start up time" + def start = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with few model group" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + it.weight = weight + } + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBS should emit log" + def logsByTime = pbsServiceWithRulesEngineModule.getLogsByTime(start) + assert getLogsByText(logsByTime, "Failed to parse rule-engine config for account $bidRequest.accountId:" + + " Weight must be greater than zero") + + where: + weight << [PBSUtils.randomNegativeNumber, 0] + } + + def "PBS should reject processing rule engine when #function schema function contain args"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = function + it.args = RuleEngineFunctionArgs.defaultFunctionArgs + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, "Failed to parse rule-engine config for account ${bidRequest.accountId}: " + + "Function '${function.value}' configuration is invalid: No arguments allowed") + + where: + function << [DEVICE_TYPE, AD_UNIT_CODE, BUNDLE, DOMAIN, TCF_IN_SCOPE, GPP_SID_AVAILABLE, FPD_AVAILABLE, + USER_FPD_AVAILABLE, EID_AVAILABLE, CHANNEL, DEVICE_COUNTRY] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy index 7c6e90d263e..517da393668 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy @@ -29,7 +29,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { private static final String PATTERN_NAME_ACCOUNT = PBSUtils.randomString private static final Map DISABLED_FILTER_SPECIFIC_PATTERN_NAME_CONFIG = getRichMediaFilterSettings(PATTERN_NAME, false) private static final Map SPECIFIC_PATTERN_NAME_CONFIG = getRichMediaFilterSettings(PATTERN_NAME) - private static final Map SNAKE_SPECIFIC_PATTERN_NAME_CONFIG = (getRichMediaFilterSettings(PATTERN_NAME) + + private static final Map SNAKE_SPECIFIC_PATTERN_NAME_CONFIG = (getRichMediaFilterSettings(PATTERN_NAME) + ["hooks.host-execution-plan": encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]).tap { endpoints.values().first().stages.values().first().groups.first.hookSequenceSnakeCase = [new HookId(moduleCodeSnakeCase: PB_RICHMEDIA_FILTER.code, hookImplCodeSnakeCase: "${PB_RICHMEDIA_FILTER.code}-${ALL_PROCESSED_BID_RESPONSES.value}-hook")] })]).collectEntries { key, value -> [(key.toString()): value.toString()] } @@ -155,7 +155,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE @@ -239,7 +239,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE @@ -323,7 +323,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE @@ -440,7 +440,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE @@ -453,10 +453,4 @@ class RichMediaFilterSpec extends ModuleBaseSpec { where: admValue << [PATTERN_NAME, "${PBSUtils.randomString}-${PATTERN_NAME}", "${PATTERN_NAME}.${PBSUtils.randomString}"] } - - private static List getAnalyticResults(BidResponse response) { - response.ext.prebid.modules?.trace?.stages?.first() - ?.outcomes?.first()?.groups?.first() - ?.invocationResults?.first()?.analyticsTags?.activities - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index 4e9bc40b590..b586688398b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -967,7 +967,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() @@ -1214,7 +1214,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy index 2575789049d..c2bf06fe50a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests.privacy +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountDsaConfig import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest @@ -14,6 +15,7 @@ import org.prebid.server.functional.model.response.auction.DsaResponse as BidDsa import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.TcfConsent + import static org.prebid.server.functional.model.request.auction.DsaPubRender.PUB_CANT_RENDER import static org.prebid.server.functional.model.request.auction.DsaPubRender.PUB_WILL_RENDER import static org.prebid.server.functional.model.request.auction.DsaRequired.NOT_REQUIRED @@ -315,7 +317,7 @@ class DsaSpec extends PrivacyBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA @@ -495,7 +497,7 @@ class DsaSpec extends PrivacyBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA @@ -535,7 +537,7 @@ class DsaSpec extends PrivacyBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 5fc0f5d7bca..299d911a398 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -267,7 +267,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_PRIVACY