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..d17db07a968 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,22 @@ package org.prebid.server.functional.model.response.auction -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.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 + 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..64a42194b59 --- /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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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..57f8659682b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy @@ -0,0 +1,178 @@ +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 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 + +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 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]) + + protected static BidRequest getDefaultBidRequestWithMultiplyBidders(DistributionChannel distributionChannel = SITE) { + BidRequest.getDefaultBidRequest(distributionChannel).tap { + 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 } + } +} 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..de900d10215 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy @@ -0,0 +1,1134 @@ +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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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: "Cache account" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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: "Cache account" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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: "Cache account" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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: "Cache account" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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..4779f77af36 --- /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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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..d8eabbaf60d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy @@ -0,0 +1,434 @@ +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 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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: "Account cache" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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..542cad335ca --- /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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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..3f63a04ec24 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy @@ -0,0 +1,909 @@ +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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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: "Account cache" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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..f8c47e4e13e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy @@ -0,0 +1,319 @@ +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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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..eb4f57707bd --- /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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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..f3271297895 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy @@ -0,0 +1,434 @@ +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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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) + + 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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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" + pbsServiceWithRulesEngineModule.sendAuctionRequest(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