diff --git a/extra/pom.xml b/extra/pom.xml
index cb3ec69cae6..cf4343a8da4 100644
--- a/extra/pom.xml
+++ b/extra/pom.xml
@@ -33,7 +33,7 @@
10.17.0
- 3.4.4
+ 3.4.2
4.5.14
2.0.1.Final
4.4
diff --git a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java
index bc4625ba905..4be70d935fc 100644
--- a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java
+++ b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java
@@ -261,10 +261,12 @@ private Bid updateBidWithId(Bid bid) {
private static BidType getType(String impId, List imps) {
for (Imp imp : imps) {
if (imp.getId().equals(impId)) {
- if (imp.getVideo() != null) {
- return BidType.video;
- } else if (imp.getAudio() != null) {
+ if (imp.getAudio() != null) {
return BidType.audio;
+ } else if (imp.getXNative() != null) {
+ return BidType.xNative;
+ } else if (imp.getVideo() != null) {
+ return BidType.video;
} else {
return BidType.banner;
}
diff --git a/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java b/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java
new file mode 100644
index 00000000000..85454c43f2d
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java
@@ -0,0 +1,141 @@
+package org.prebid.server.bidder.zeta_global_ssp;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.response.Bid;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import org.prebid.server.bidder.Bidder;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderCall;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.HttpRequest;
+import org.prebid.server.bidder.model.Result;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.json.DecodeException;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp.ExtImpZetaGlobalSSP;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
+import org.prebid.server.util.BidderUtil;
+import org.prebid.server.util.HttpUtil;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class ZetaGlobalSspBidder implements Bidder {
+
+ private static final TypeReference> ZETA_GLOBAL_EXT_TYPE_REFERENCE =
+ new TypeReference<>() {
+ };
+
+ private static final TypeReference> EXT_BID_TYPE_REFERENCE =
+ new TypeReference<>() {
+ };
+ private static final String SID_MACRO = "{{AccountID}}";
+
+ private final String endpointUrl;
+ private final JacksonMapper mapper;
+
+ public ZetaGlobalSspBidder(String endpointUrl, JacksonMapper mapper) {
+ this.endpointUrl = HttpUtil.validateUrl(endpointUrl);
+ this.mapper = Objects.requireNonNull(mapper);
+ }
+
+ @Override
+ public Result>> makeHttpRequests(BidRequest request) {
+ final Imp firstImp = request.getImp().getFirst();
+ final ExtImpZetaGlobalSSP extImp;
+
+ try {
+ extImp = parseImpExt(firstImp);
+ } catch (PreBidException e) {
+ return Result.withError(BidderError.badInput(e.getMessage()));
+ }
+
+ final HttpRequest httpRequest = BidderUtil.defaultRequest(
+ removeImpsExt(request),
+ resolveEndpoint(extImp),
+ mapper);
+
+ return Result.withValues(Collections.singletonList(httpRequest));
+ }
+
+ private ExtImpZetaGlobalSSP parseImpExt(Imp imp) {
+ try {
+ return mapper.mapper().convertValue(imp.getExt(), ZETA_GLOBAL_EXT_TYPE_REFERENCE).getBidder();
+ } catch (IllegalArgumentException e) {
+ throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId());
+ }
+ }
+
+ private String resolveEndpoint(ExtImpZetaGlobalSSP extImpZetaGlobalSSP) {
+ return endpointUrl
+ .replace(SID_MACRO, Objects.toString(extImpZetaGlobalSSP.getSid(), "0"));
+ }
+
+ private BidRequest removeImpsExt(BidRequest request) {
+ final List imps = new ArrayList<>(request.getImp());
+ final Imp firstImp = imps.getFirst().toBuilder().ext(null).build();
+ imps.set(0, firstImp);
+
+ return request.toBuilder()
+ .imp(imps)
+ .build();
+ }
+
+ @Override
+ public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) {
+ try {
+ final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+ final List errors = new ArrayList<>();
+ return Result.of(extractBids(bidResponse, errors), errors);
+ } catch (DecodeException | PreBidException e) {
+ return Result.withError(BidderError.badServerResponse(e.getMessage()));
+ }
+ }
+
+ private List extractBids(BidResponse bidResponse, List errors) {
+ if (bidResponse == null || bidResponse.getSeatbid() == null) {
+ return Collections.emptyList();
+ }
+ return bidsFromResponse(bidResponse, errors);
+ }
+
+ private List bidsFromResponse(BidResponse bidResponse, List errors) {
+ return bidResponse.getSeatbid().stream()
+ .filter(Objects::nonNull)
+ .map(SeatBid::getBid)
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .filter(Objects::nonNull)
+ .map(bid -> makeBid(bid, bidResponse.getCur(), errors))
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ private BidderBid makeBid(Bid bid, String currency, List errors) {
+ final BidType mediaType = getMediaType(bid, errors);
+ return mediaType == null ? null : BidderBid.of(bid, mediaType, currency);
+ }
+
+ private BidType getMediaType(Bid bid, List errors) {
+ try {
+ return Optional.ofNullable(bid.getExt())
+ .map(ext -> mapper.mapper().convertValue(ext, EXT_BID_TYPE_REFERENCE))
+ .map(ExtPrebid::getPrebid)
+ .map(ExtBidPrebid::getType)
+ .orElseThrow(IllegalArgumentException::new);
+ } catch (IllegalArgumentException e) {
+ errors.add(BidderError.badServerResponse(
+ "Failed to parse impression \"%s\" mediatype".formatted(bid.getImpid())));
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
index d492981f568..6389139a5ac 100644
--- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
+++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
@@ -21,6 +21,8 @@
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.metric.MetricName;
+import org.prebid.server.metric.Metrics;
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
@@ -50,19 +52,35 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {
private static final int MODEL_WEIGHT_MAX_VALUE = 100;
private static final int MODEL_WEIGHT_MIN_VALUE = 1;
+ private static final String FETCH_FAILED_ERROR_MESSAGE = "Price floors processing failed: %s. "
+ + "Following parsing of request price floors is failed: %s";
+ private static final String DYNAMIC_DATA_NOT_ALLOWED_MESSAGE =
+ "Price floors processing failed: Using dynamic data is not allowed. "
+ + "Following parsing of request price floors is failed: %s";
+ private static final String INVALID_REQUEST_WARNING_MESSAGE =
+ "Price floors processing failed: parsing of request price floors is failed: %s";
+ private static final String ERROR_LOG_MESSAGE =
+ "Price Floors can't be resolved for account %s and request %s, reason: %s";
+
private final PriceFloorFetcher floorFetcher;
private final PriceFloorResolver floorResolver;
+ private final Metrics metrics;
private final JacksonMapper mapper;
+ private final double logSamplingRate;
private final RandomWeightedEntrySupplier modelPicker;
public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
PriceFloorResolver floorResolver,
- JacksonMapper mapper) {
+ Metrics metrics,
+ JacksonMapper mapper,
+ double logSamplingRate) {
this.floorFetcher = Objects.requireNonNull(floorFetcher);
this.floorResolver = Objects.requireNonNull(floorResolver);
+ this.metrics = Objects.requireNonNull(metrics);
this.mapper = Objects.requireNonNull(mapper);
+ this.logSamplingRate = logSamplingRate;
modelPicker = new RandomPositiveWeightedEntrySupplier<>(BasicPriceFloorProcessor::resolveModelGroupWeight);
}
@@ -82,7 +100,7 @@ public BidRequest enrichWithPriceFloors(BidRequest bidRequest,
return disableFloorsForRequest(bidRequest);
}
- final PriceFloorRules floors = resolveFloors(account, bidRequest, errors);
+ final PriceFloorRules floors = resolveFloors(account, bidRequest, warnings);
return updateBidRequestWithFloors(bidRequest, bidder, floors, errors, warnings);
}
@@ -122,49 +140,13 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) {
return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors);
}
- private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List errors) {
+ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List warnings) {
final PriceFloorRules requestFloors = extractRequestFloors(bidRequest);
final FetchResult fetchResult = floorFetcher.fetch(account);
- final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus);
-
- if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) {
- final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
- return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
- }
+ final FetchStatus fetchStatus = fetchResult.getFetchStatus();
- if (requestFloors != null) {
- try {
- final Optional priceFloorsConfig = Optional.of(account)
- .map(Account::getAuction)
- .map(AccountAuctionConfig::getPriceFloors);
-
- final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
- .orElse(null);
- final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
- .orElse(null);
-
- PriceFloorRulesValidator.validateRules(
- requestFloors,
- PriceFloorsConfigResolver.resolveMaxValue(maxRules),
- PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));
-
- return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request);
- } catch (PreBidException e) {
- errors.add("Failed to parse price floors from request, with a reason: %s".formatted(e.getMessage()));
- conditionalLogger.error(
- "Failed to parse price floors from request with id: '%s', with a reason: %s"
- .formatted(bidRequest.getId(), e.getMessage()),
- 0.01d);
- }
- }
-
- return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData);
- }
-
- private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) {
- final boolean isUsingDynamicDataAllowed = Optional.of(account)
- .map(Account::getAuction)
+ final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account.getAuction())
.map(AccountAuctionConfig::getPriceFloors)
.map(AccountPriceFloorsConfig::getUseDynamicData)
.map(BooleanUtils::isNotFalse)
@@ -175,12 +157,68 @@ private static boolean shouldUseDynamicData(Account account, FetchResult fetchRe
.map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate)
.orElse(true);
- return isUsingDynamicDataAllowed && shouldUseDynamicData;
+ if (fetchStatus == FetchStatus.success && isUsingDynamicDataAllowed && shouldUseDynamicData) {
+ final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
+ return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
+ }
+
+ return requestFloors == null
+ ? createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData)
+ : getPriceFloorRules(
+ bidRequest, account, requestFloors, fetchResult, isUsingDynamicDataAllowed, warnings);
+ }
+
+ private PriceFloorRules getPriceFloorRules(BidRequest bidRequest,
+ Account account,
+ PriceFloorRules requestFloors,
+ FetchResult fetchResult,
+ boolean isDynamicDataAllowed,
+ List warnings) {
+
+ try {
+ final Optional priceFloorsConfig = Optional.of(account.getAuction())
+ .map(AccountAuctionConfig::getPriceFloors);
+
+ final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
+ .orElse(null);
+ final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
+ .orElse(null);
+
+ PriceFloorRulesValidator.validateRules(
+ requestFloors,
+ PriceFloorsConfigResolver.resolveMaxValue(maxRules),
+ PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));
+
+ return createFloorsFrom(requestFloors, fetchResult.getFetchStatus(), PriceFloorLocation.request);
+ } catch (PreBidException e) {
+ logErrorMessage(fetchResult, isDynamicDataAllowed, e, account.getId(), bidRequest.getId(), warnings);
+ return createFloorsFrom(null, fetchResult.getFetchStatus(), PriceFloorLocation.noData);
+ }
}
- private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
- PriceFloorData providerRulesData) {
+ private void logErrorMessage(FetchResult fetchResult,
+ boolean isDynamicDataAllowed,
+ PreBidException requestFloorsValidationException,
+ String accountId,
+ String requestId,
+ List warnings) {
+
+ final String validationMessage = requestFloorsValidationException.getMessage();
+ final String errorMessage = switch (fetchResult.getFetchStatus()) {
+ case inprogress -> null;
+ case error, timeout, none -> FETCH_FAILED_ERROR_MESSAGE.formatted(
+ fetchResult.getErrorMessage(), validationMessage);
+ case success -> isDynamicDataAllowed ? null : DYNAMIC_DATA_NOT_ALLOWED_MESSAGE.formatted(validationMessage);
+ };
+
+ if (errorMessage != null) {
+ warnings.add(INVALID_REQUEST_WARNING_MESSAGE.formatted(validationMessage));
+ conditionalLogger.error(ERROR_LOG_MESSAGE.formatted(accountId, requestId, errorMessage), logSamplingRate);
+ metrics.updateAlertsMetrics(MetricName.general);
+ }
+ }
+ private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, PriceFloorData providerRulesData) {
final Price floorMinPrice = resolveFloorMinPrice(requestFloors);
return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder())
diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
index b7d4ac4185f..b1cc8c257b2 100644
--- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
+++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
@@ -90,7 +90,10 @@ public FetchResult fetch(Account account) {
final AccountFetchContext accountFetchContext = fetchedData.get(account.getId());
return accountFetchContext != null
- ? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus())
+ ? FetchResult.of(
+ accountFetchContext.getRulesData(),
+ accountFetchContext.getFetchStatus(),
+ accountFetchContext.getErrorMessage())
: fetchPriceFloorData(account);
}
@@ -99,20 +102,20 @@ private FetchResult fetchPriceFloorData(Account account) {
final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled);
if (BooleanUtils.isFalse(fetchEnabled)) {
- return FetchResult.of(null, FetchStatus.none);
+ return FetchResult.none("Fetching is disabled");
}
final String accountId = account.getId();
final String fetchUrl = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getUrl);
if (!isUrlValid(fetchUrl)) {
- logger.error("Malformed fetch.url: '%s', passed for account %s".formatted(fetchUrl, accountId));
- return FetchResult.of(null, FetchStatus.error);
+ logger.error("Malformed fetch.url: '%s' passed for account %s".formatted(fetchUrl, accountId));
+ return FetchResult.error("Malformed fetch.url '%s' passed".formatted(fetchUrl));
}
if (!fetchInProgress.contains(accountId)) {
fetchPriceFloorDataAsynchronous(fetchConfig, accountId);
}
- return FetchResult.of(null, FetchStatus.inprogress);
+ return FetchResult.inProgress();
}
private boolean isUrlValid(String url) {
@@ -148,7 +151,7 @@ private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetch
fetchInProgress.add(accountId);
httpClient.get(fetchUrl, timeout, resolveMaxFileSize(maxFetchFileSizeKb))
- .map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId))
+ .map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig))
.recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId))
.map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId))
.map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId));
@@ -159,23 +162,20 @@ private static long resolveMaxFileSize(Long maxSizeInKBytes) {
}
private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientResponse,
- AccountPriceFloorsFetchConfig fetchConfig,
- String accountId) {
+ AccountPriceFloorsFetchConfig fetchConfig) {
final int statusCode = httpClientResponse.getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
- throw new PreBidException("Failed to request for account %s, provider respond with status %s"
- .formatted(accountId, statusCode));
+ throw new PreBidException("Failed to request, provider respond with status %s".formatted(statusCode));
}
final String body = httpClientResponse.getBody();
if (StringUtils.isBlank(body)) {
- throw new PreBidException(
- "Failed to parse price floor response for account %s, response body can not be empty"
- .formatted(accountId));
+ throw new PreBidException("Failed to parse price floor response, response body can not be empty");
}
- final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId);
+ final PriceFloorData priceFloorData = parsePriceFloorData(body);
+
PriceFloorRulesValidator.validateRulesData(
priceFloorData,
PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()),
@@ -183,16 +183,17 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon
return ResponseCacheInfo.of(priceFloorData,
FetchStatus.success,
+ null,
cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl()));
}
- private PriceFloorData parsePriceFloorData(String body, String accountId) {
+ private PriceFloorData parsePriceFloorData(String body) {
final PriceFloorData priceFloorData;
try {
priceFloorData = mapper.decodeValue(body, PriceFloorData.class);
} catch (DecodeException e) {
- throw new PreBidException("Failed to parse price floor response for account %s, cause: %s"
- .formatted(accountId, ExceptionUtils.getMessage(e)));
+ throw new PreBidException(
+ "Failed to parse price floor response, cause: %s".formatted(ExceptionUtils.getMessage(e)));
}
return priceFloorData;
}
@@ -220,8 +221,11 @@ private PriceFloorData updateCache(ResponseCacheInfo cacheInfo,
String accountId) {
final long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig));
- final AccountFetchContext fetchContext =
- AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId);
+ final AccountFetchContext fetchContext = AccountFetchContext.of(
+ cacheInfo.getRulesData(),
+ cacheInfo.getFetchStatus(),
+ cacheInfo.getErrorMessage(),
+ maxAgeTimerId);
if (cacheInfo.getFetchStatus() == FetchStatus.success || !fetchedData.containsKey(accountId)) {
fetchedData.put(accountId, fetchContext);
@@ -274,23 +278,24 @@ private Future recoverFromFailedFetching(Throwable throwable,
metrics.updatePriceFloorFetchMetric(MetricName.failure);
final FetchStatus fetchStatus;
+ final String errorMessage;
if (throwable instanceof TimeoutException || throwable instanceof ConnectTimeoutException) {
fetchStatus = FetchStatus.timeout;
- logger.error("Fetch price floor request timeout for fetch.url: '%s', account %s exceeded."
- .formatted(fetchUrl, accountId));
+ errorMessage = "Fetch price floor request timeout for fetch.url '%s' exceeded.".formatted(fetchUrl);
} else {
fetchStatus = FetchStatus.error;
- logger.error(
- "Failed to fetch price floor from provider for fetch.url: '%s', account = %s with a reason : %s "
- .formatted(fetchUrl, accountId, throwable.getMessage()));
+ errorMessage = "Failed to fetch price floor from provider for fetch.url '%s', with a reason: %s"
+ .formatted(fetchUrl, throwable.getMessage());
}
- return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus));
+ logger.error("Price floor fetching failed for account %s: %s".formatted(accountId, errorMessage));
+ return Future.succeededFuture(ResponseCacheInfo.withError(fetchStatus, errorMessage));
}
private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {
+
final long accountPeriodicTimeSec =
ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec);
final long periodicTimeSec =
@@ -318,6 +323,8 @@ private static class AccountFetchContext {
FetchStatus fetchStatus;
+ String errorMessage;
+
Long maxAgeTimerId;
}
@@ -328,10 +335,12 @@ private static class ResponseCacheInfo {
FetchStatus fetchStatus;
+ String errorMessage;
+
Long cacheTtl;
- public static ResponseCacheInfo withStatus(FetchStatus status) {
- return ResponseCacheInfo.of(null, status, null);
+ public static ResponseCacheInfo withError(FetchStatus status, String errorMessage) {
+ return ResponseCacheInfo.of(null, status, errorMessage, null);
}
}
}
diff --git a/src/main/java/org/prebid/server/floors/proto/FetchResult.java b/src/main/java/org/prebid/server/floors/proto/FetchResult.java
index 36c4fda58e0..336bb26ee43 100644
--- a/src/main/java/org/prebid/server/floors/proto/FetchResult.java
+++ b/src/main/java/org/prebid/server/floors/proto/FetchResult.java
@@ -9,4 +9,18 @@ public class FetchResult {
PriceFloorData rulesData;
FetchStatus fetchStatus;
+
+ String errorMessage;
+
+ public static FetchResult none(String errorMessage) {
+ return FetchResult.of(null, FetchStatus.none, errorMessage);
+ }
+
+ public static FetchResult error(String errorMessage) {
+ return FetchResult.of(null, FetchStatus.error, errorMessage);
+ }
+
+ public static FetchResult inProgress() {
+ return FetchResult.of(null, FetchStatus.inprogress, null);
+ }
}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java
new file mode 100644
index 00000000000..b904d2e677a
--- /dev/null
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java
@@ -0,0 +1,9 @@
+package org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp;
+
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class ExtImpZetaGlobalSSP {
+
+ Integer sid;
+}
diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java
index 8a483e92a4d..16a79d6c0f6 100644
--- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java
@@ -20,6 +20,7 @@
import org.prebid.server.metric.Metrics;
import org.prebid.server.settings.ApplicationSettings;
import org.prebid.server.vertx.httpclient.HttpClient;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@@ -84,9 +85,11 @@ PriceFloorResolver noOpPriceFloorResolver() {
@ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true")
PriceFloorProcessor basicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
PriceFloorResolver floorResolver,
- JacksonMapper mapper) {
+ Metrics metrics,
+ JacksonMapper mapper,
+ @Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
- return new BasicPriceFloorProcessor(floorFetcher, floorResolver, mapper);
+ return new BasicPriceFloorProcessor(floorFetcher, floorResolver, metrics, mapper, logSamplingRate);
}
@Bean
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java
new file mode 100644
index 00000000000..aa98e645aa0
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java
@@ -0,0 +1,43 @@
+package org.prebid.server.spring.config.bidder;
+
+import org.prebid.server.bidder.BidderDeps;
+import org.prebid.server.bidder.zeta_global_ssp.ZetaGlobalSspBidder;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
+import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
+import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
+import org.prebid.server.spring.env.YamlPropertySourceFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+import jakarta.validation.constraints.NotBlank;
+
+@Configuration
+@PropertySource(value = "classpath:/bidder-config/zeta_global_ssp.yaml", factory = YamlPropertySourceFactory.class)
+public class ZetaGlobalSspConfiguration {
+
+ private static final String BIDDER_NAME = "zeta_global_ssp";
+
+ @Bean("zetaglobalsspConfigurationProperties")
+ @ConfigurationProperties("adapters.zetaglobalssp")
+ BidderConfigurationProperties configurationProperties() {
+ return new BidderConfigurationProperties();
+ }
+
+ @Bean
+ BidderDeps zetaGlobalSspBidderDeps(@Qualifier("zetaglobalsspConfigurationProperties")
+ BidderConfigurationProperties zetaGlobalSspConfigurationProperties,
+ @NotBlank @Value("${external-url}") String externalUrl,
+ JacksonMapper mapper) {
+
+ return BidderDepsAssembler.forBidder(BIDDER_NAME)
+ .withConfig(zetaGlobalSspConfigurationProperties)
+ .usersyncerCreator(UsersyncerCreator.create(externalUrl))
+ .bidderCreator(config -> new ZetaGlobalSspBidder(config.getEndpoint(), mapper))
+ .assemble();
+ }
+}
diff --git a/src/main/resources/bidder-config/epsilon.yaml b/src/main/resources/bidder-config/epsilon.yaml
index ba7472331d7..44970a25db2 100644
--- a/src/main/resources/bidder-config/epsilon.yaml
+++ b/src/main/resources/bidder-config/epsilon.yaml
@@ -12,10 +12,12 @@ adapters:
- banner
- video
- audio
+ - native
site-media-types:
- banner
- video
- audio
+ - native
supported-vendors:
vendor-id: 24
usersync:
diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml
index 4aa01b82bbb..a6522be1122 100644
--- a/src/main/resources/bidder-config/generic.yaml
+++ b/src/main/resources/bidder-config/generic.yaml
@@ -41,27 +41,6 @@ adapters:
- video
supported-vendors:
vendor-id: 0
- zeta_global_ssp:
- enabled: false
- endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA
- endpoint-compression: gzip
- meta-info:
- maintainer-email: DL-Zeta-SSP@zetaglobal.com
- app-media-types:
- - banner
- - video
- site-media-types:
- - banner
- - video
- supported-vendors:
- vendor-id: 833
- usersync:
- enabled: true
- cookie-family-name: zeta_global_ssp
- redirect:
- url: https://ssp.disqus.com/redirectuser?sid=GET_SID_FROM_ZETA&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}}
- uid-macro: 'BUYERUID'
- support-cors: false
blue:
enabled: false
endpoint: https://prebid-us-east-1.getblue.io/?src=prebid
diff --git a/src/main/resources/bidder-config/zeta_global_ssp.yaml b/src/main/resources/bidder-config/zeta_global_ssp.yaml
new file mode 100644
index 00000000000..4c050049d2e
--- /dev/null
+++ b/src/main/resources/bidder-config/zeta_global_ssp.yaml
@@ -0,0 +1,24 @@
+adapters:
+ zeta_global_ssp:
+ endpoint: https://ssp.disqus.com/bid/prebid-server?sid={{AccountID}}
+ endpoint-compression: gzip
+ geoscope:
+ - global
+ modifying-vast-xml-allowed: true
+ meta-info:
+ maintainer-email: DL-Zeta-SSP@zetaglobal.com
+ app-media-types:
+ - banner
+ - video
+ - audio
+ site-media-types:
+ - banner
+ - video
+ - audio
+ vendor-id: 833
+ usersync:
+ cookie-family-name: zeta_global_ssp
+ redirect:
+ url: https://ssp.disqus.com/redirectuser?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}}
+ support-cors: false
+ uid-macro: 'BUYERUID'
diff --git a/src/main/resources/static/bidder-params/zeta_global_ssp.json b/src/main/resources/static/bidder-params/zeta_global_ssp.json
index 91ff05ed089..8a6d1d0a060 100644
--- a/src/main/resources/static/bidder-params/zeta_global_ssp.json
+++ b/src/main/resources/static/bidder-params/zeta_global_ssp.json
@@ -1,10 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Zeta Global SSP Adapter Params",
- "description": "A schema which validates params accepted by the Zeta SSP adapter",
- "type": "object",
-
- "properties": {},
+ "description": "A schema which validates params accepted by the Zeta Global SSP adapter",
- "required": []
+ "type": "object",
+ "properties": {
+ "sid": {
+ "type": "integer",
+ "description": "An ID which identifies the publisher"
+ }
+ }
}
diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy
index 490c1456e53..80d779e069d 100644
--- a/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy
@@ -7,7 +7,6 @@ import org.prebid.server.functional.model.request.setuid.SetuidRequest
enum Metric {
- ALERT_GENERAL("alerts.general"),
PROCESSED_ACTIVITY_RULES_COUNT("requests.activity.processedrules.count"),
ACCOUNT_PROCESSED_RULES_COUNT("requests.activity.processedrules.count"),
TEMPLATE_ADAPTER_DISALLOWED_COUNT("adapter.{bidderName}.activity.{activityType}.disallowed.count"),
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy
index 22e29b76908..127ed32bfd9 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy
@@ -60,7 +60,7 @@ class Bid implements ObjectMapperWrapper {
new Bid().tap {
id = UUID.randomUUID()
impid = imp.id
- price = PBSUtils.getRandomPrice()
+ price = imp.bidFloor != null ? imp.bidFloor : PBSUtils.getRandomPrice()
crid = 1
height = imp.banner && imp.banner.format ? imp.banner.format.first().height : null
weight = imp.banner && imp.banner.format ? imp.banner.format.first().weight : null
diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy
index 63ba2516ff4..a4fda13f829 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy
@@ -40,6 +40,7 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper {
private static final int MIN_TIMEOUT = DEFAULT_TIMEOUT
private static final int DEFAULT_TARGETING_PRECISION = 1
private static final String DEFAULT_CACHE_DIRECTORY = "/app/prebid-server/data"
+ protected static final String ALERT_GENERAL = "alerts.general"
protected static final Map GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true",
"adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()]
diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy
index 579a8e16597..9dd17c31e0a 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy
@@ -63,7 +63,7 @@ class BidValidationSpec extends BaseSpec {
and: "Bid validation metric value is incremented"
def metrics = strictPrebidService.sendCollectedMetricsRequest()
- assert metrics["alerts.general"] == 1
+ assert metrics[ALERT_GENERAL] == 1
where:
bidRequest << [BidRequest.getDefaultBidRequest(DistributionChannel.APP).tap {
@@ -105,7 +105,7 @@ class BidValidationSpec extends BaseSpec {
and: "Bid validation metric value is incremented"
def metrics = softPrebidService.sendCollectedMetricsRequest()
- assert metrics["alerts.general"] == 1
+ assert metrics[ALERT_GENERAL] == 1
and: "PBS log should contain message"
def logs = softPrebidService.getLogsByTime(startTime)
diff --git a/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy
index 92ef175681e..ee3e808a56e 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy
@@ -146,7 +146,7 @@ class MetricsSpec extends BaseSpec {
assert metrics["adapter.generic.requests.type.openrtb2-dooh" as String] == 1
and: "alert.general metric should be updated"
- assert metrics["alerts.general" as String] == 1
+ assert metrics[ALERT_GENERAL] == 1
and: "Other channel types should not be populated"
assert !metrics["account.${accountId}.requests.type.openrtb2-web" as String]
@@ -175,7 +175,7 @@ class MetricsSpec extends BaseSpec {
assert metrics["adapter.generic.requests.type.openrtb2-app" as String] == 1
and: "alert.general metric should be updated"
- assert metrics["alerts.general" as String] == 1
+ assert metrics[ALERT_GENERAL] == 1
and: "Other channel types should not be populated"
assert !metrics["account.${accountId}.requests.type.openrtb2-dooh" as String]
diff --git a/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy
index 8e6d7f1a57d..97a0015cea5 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy
@@ -444,7 +444,7 @@ class OpenxSpec extends BaseSpec {
and: "Alert.general metric should be updated"
def metrics = pbsService.sendCollectedMetricsRequest()
- assert metrics["alerts.general" as String] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS shouldn't populate fledge or igi config when bidder respond with igb"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy
index 8b3f5d936bd..a604264b264 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy
@@ -15,6 +15,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest
import org.prebid.server.functional.model.request.auction.BidRequestExt
import org.prebid.server.functional.model.request.auction.DistributionChannel
import org.prebid.server.functional.model.request.auction.ExtPrebidFloors
+import org.prebid.server.functional.model.request.auction.FetchStatus
import org.prebid.server.functional.model.request.auction.Prebid
import org.prebid.server.functional.model.request.auction.Video
import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse
@@ -34,17 +35,34 @@ abstract class PriceFloorsBaseSpec extends BaseSpec {
public static final BigDecimal FLOOR_MIN = 0.5
public static final BigDecimal FLOOR_MAX = 2
- public static final Map FLOORS_CONFIG = ["price-floors.enabled" : "true",
- "settings.default-account-config": encode(defaultAccountConfigSettings)]
+ public static final Map FLOORS_CONFIG = ["price-floors.enabled": "true"]
+
+ protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer)
protected static final String BASIC_FETCH_URL = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT
protected static final int MAX_MODEL_WEIGHT = 100
+ protected static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" }
+
+ protected static final Closure URL_EMPTY_ERROR = { url -> "Failed to fetch price floor from provider for fetch.url '${url}'"
+ }
+ protected static final String FETCHING_DISABLED_ERROR = "Fetching is disabled"
+ protected static final Closure PRICE_FLOORS_ERROR_LOG = { bidRequest, reason, warningMessage ->
+ "Price Floors can't be resolved for account ${bidRequest.accountId} and request ${bidRequest.id}, reason: ${PRICE_FLOORS_WARNING_MESSAGE(reason, warningMessage)}"
+ }
+ protected static final Closure WARNING_MESSAGE = { message ->
+ "Price floors processing failed: parsing of request price floors is failed: $message"
+ }
+ protected static final Closure FETCHING_FLOORS_ERROR_LOG = { bidRequest, warningMessage ->
+ "Price floor fetching failed for account ${bidRequest.accountId}: ${URL_EMPTY_ERROR("$BASIC_FETCH_URL${bidRequest.accountId}")}, with a reason: $warningMessage"
+ }
+ private static final Closure PRICE_FLOORS_WARNING_MESSAGE = { reason, details ->
+ "Price floors processing failed: $reason. Following parsing of request price floors is failed: $details"
+ }
private static final int DEFAULT_MODEL_WEIGHT = 1
private static final int CURRENCY_CONVERSION_PRECISION = 3
private static final int FLOOR_VALUE_PRECISION = 4
- protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer)
protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG)
def setupSpec() {
@@ -121,8 +139,9 @@ abstract class PriceFloorsBaseSpec extends BaseSpec {
protected void cacheFloorsProviderRules(BidRequest bidRequest,
PrebidServerService pbsService = floorsPbsService,
- BidderName bidderName = BidderName.GENERIC) {
- PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value]?.first?.ext?.prebid?.floors?.fetchStatus != INPROGRESS },
+ BidderName bidderName = BidderName.GENERIC,
+ FetchStatus fetchStatus = INPROGRESS) {
+ PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value]?.first?.ext?.prebid?.floors?.fetchStatus != fetchStatus },
5000,
1000)
}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy
index c5ce101b156..b40f6e8cac2 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy
@@ -17,11 +17,13 @@ import java.time.Instant
import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400
import static org.prebid.server.functional.model.Currency.EUR
import static org.prebid.server.functional.model.Currency.JPY
+import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE
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.SITE
import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR
+import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS
import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE
import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS
import static org.prebid.server.functional.model.request.auction.Location.FETCH
@@ -43,8 +45,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
private static final int DEFAULT_FLOOR_VALUE_MIN = 0
private static final int FLOOR_MIN = 0
- private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" }
private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure"
+ private static final String PRICE_FLOOR_VALUES_MISSING = 'Price floor rules values can\'t be null or empty, but were null'
+ private static final String MODEL_WEIGHT_INVALID = "Price floor modelGroup modelWeight must be in range(1-100), but was %s"
+ private static final String SKIP_RATE_INVALID = "Price floor modelGroup skipRate must be in range(0-100), but was %s"
+ private static Instant startTime
+
+ def setupSpec() {
+ startTime = Instant.now()
+ }
def "PBS should activate floors feature when price-floors.enabled = true in PBS config"() {
given: "Pbs with PF configuration"
@@ -121,7 +130,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def logs = floorsPbsService.getLogsByTime(startTime)
def floorsLogs = getLogsByText(logs, bidRequest.accountId)
assert floorsLogs.size() == 1
- assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.accountId")
+ assert floorsLogs.first().contains("alformed fetch.url: 'null' passed for account $bidRequest.accountId")
and: "PBS floors validation failure should not reject the entire auction"
assert !response.seatbid.isEmpty()
@@ -525,7 +534,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def startTime = Instant.now()
and: "Default BidRequest"
- def bidRequest = BidRequest.defaultBidRequest
+ def bidRequest = BidRequest.getDefaultBidRequest()
and: "Account with enabled fetch and fetch.url in the DB"
def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
@@ -545,7 +554,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
and: "PBS fetch rules from floors provider"
- cacheFloorsProviderRules(bidRequest, floorsPbsService)
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -554,14 +563,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def metrics = floorsPbsService.sendCollectedMetricsRequest()
assert metrics[FETCH_FAILURE_METRIC] == 1
- then: "PBS should fetch data"
+ and: "PBS should fetch data"
assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
+ and: "PBS should not add warning or errors"
+ assert !response.ext.warnings
+ assert !response.ext.errors
+
and: "PBS log should contain error"
def logs = floorsPbsService.getLogsByTime(startTime)
+ def message = "Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate"
def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("reason : Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -589,6 +603,90 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
]
}
+ def "PBS should log merged error and increase metrics when useFetchDataRate have invalid value from provider and request"() {
+ given: "BidRequest with invalid floors"
+ def requestUseFetchDataRate = PBSUtils.getRandomNegativeNumber()
+ def bidRequest = getBidRequestWithFloors().tap {
+ it.ext.prebid.floors.data.useFetchDataRate = requestUseFetchDataRate
+ }
+
+ and: "Account with enabled fetch and fetch.url in the DB"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ config.auction.priceFloors.useDynamicData = true
+ }
+ accountDao.save(account)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Set Floors Provider response"
+ def providerUseFetchDataRate = PBSUtils.getRandomNegativeNumber()
+ def floorValue = PBSUtils.randomFloorValue
+ def floorsResponse = PriceFloorData.priceFloorData.tap {
+ modelGroups[0].values = [(rule): floorValue]
+ useFetchDataRate = providerUseFetchDataRate
+ }
+ floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
+
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, ERROR)
+
+ and: "Test start time"
+ def startTime = Instant.now()
+
+ when: "PBS processes auction request"
+ def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "metric should be updated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[FETCH_FAILURE_METRIC] == 1
+
+ and: "PBS should add single warning"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message ==
+ [WARNING_MESSAGE("Price floor data useFetchDataRate must be in range(0-100), but was $requestUseFetchDataRate")]
+
+ and: "PBS should not add error to request"
+ assert !response.ext.errors
+
+ and: "PBS should fetch data"
+ assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
+
+ and: "PBS log should contain combined error log"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def fetchingErrorLogs = getLogsByText(logs, "Price Floors can't be resolved for account $bidRequest.accountId " +
+ "and request $bidRequest.id, reason: Price floors processing failed: Failed to fetch price floor from provider " +
+ "for fetch.url '$BASIC_FETCH_URL${bidRequest.accountId}', with a reason: " +
+ "Price floor data useFetchDataRate must be in range(0-100), but was $providerUseFetchDataRate. " +
+ "Following parsing of request price floors is failed: " +
+ "Price floor data useFetchDataRate must be in range(0-100), but was $requestUseFetchDataRate")
+ assert fetchingErrorLogs.size() == 1
+
+ and: "Floors validation failure cannot reject the entire auction"
+ assert !response.seatbid?.isEmpty()
+
+ and: "Bidder request should contain floors data from floors provider"
+ def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
+ verifyAll(bidderRequest) {
+ imp.bidFloor == bidRequest.imp.bidFloor
+ imp.bidFloorCur == bidRequest.imp.bidFloorCur
+
+ !imp[0].ext?.prebid?.floors?.floorRule
+ !imp[0].ext?.prebid?.floors?.floorRuleValue
+ !imp[0].ext?.prebid?.floors?.floorValue
+
+ !ext?.prebid?.floors?.floorProvider
+ !ext?.prebid?.floors?.skipRate
+ !ext?.prebid?.floors?.data
+ ext?.prebid?.floors?.location == NO_DATA
+ ext?.prebid?.floors?.fetchStatus == ERROR
+ }
+ }
+
def "PBS should process floors from request when use-dynamic-data = false"() {
given: "Pbs with PF configuration with useDynamicData"
def defaultAccountConfigSettings = defaultAccountConfigSettings.tap {
@@ -645,7 +743,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
floorsProvider.setResponse(accountId, BAD_REQUEST_400)
and: "PBS fetch rules from floors provider"
- cacheFloorsProviderRules(bidRequest, floorsPbsService)
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -657,12 +755,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Failed to request, provider respond with status 400"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to request for " +
- "account $accountId, provider respond with status 400")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -687,6 +784,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def invalidJson = "{{}}"
floorsProvider.setResponse(accountId, invalidJson)
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -697,12 +797,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Failed to parse price floor response, cause: DecodeException: Failed to decode"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " +
- "response for account $accountId, cause: DecodeException: Failed to decode")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -726,6 +825,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "Set Floors Provider response with invalid json"
floorsProvider.setResponse(accountId, "")
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -736,12 +838,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Failed to parse price floor response, response body can not be empty"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " +
- "response for account $accountId, response body can not be empty" as String)
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -768,6 +869,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
floorsProvider.setResponse(accountId, floorsResponse)
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -778,12 +882,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Price floor rules should contain at least one model group"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules should contain " +
- "at least one model group " as String)
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -810,6 +913,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
floorsProvider.setResponse(accountId, floorsResponse)
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -821,11 +927,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS log should contain error"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules values can't " +
- "be null or empty, but were null" as String)
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, PRICE_FLOOR_VALUES_MISSING))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -855,6 +959,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
floorsProvider.setResponse(accountId, floorsResponse)
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -865,12 +972,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Price floor rules number 2 exceeded its maximum number $maxRules"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules number " +
- "2 exceeded its maximum number $maxRules")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -901,6 +1007,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "Set Floors Provider response with timeout"
floorsProvider.setResponseWithTimeout(accountId)
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, pbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = pbsService.sendAuctionRequest(bidRequest)
@@ -912,10 +1021,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS log should contain error"
def logs = pbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
+ def floorsLogs = getLogsByText(logs, "Price floor fetching failed for account $bidRequest.accountId: " +
+ "Fetch price floor request timeout for fetch.url '$BASIC_FETCH_URL$accountId' exceeded")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$BASIC_FETCH_URL$accountId', " +
- "account $accountId exceeded")
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -944,6 +1052,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def responseSize = convertKilobyteSizeToByte(maxSize) + 75
floorsProvider.setResponse(accountId, floorsResponse, ["Content-Length": responseSize as String])
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -954,12 +1065,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Response size $responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Response size " +
- "$responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -1277,6 +1387,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1284,11 +1398,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor floorMin " +
- "must be positive float, but was $invalidFloorMin"]
+ and: "Response should contain warning"
+ def message = "Price floor floorMin must be positive float, but was $invalidFloorMin"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
}
def "PBS should validate rules from request when request doesn't contain modelGroups"() {
@@ -1305,6 +1418,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1312,11 +1429,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor rules " +
- "should contain at least one model group"]
+ and: "Response should contain warning"
+ def message = "Price floor rules should contain at least one model group"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
}
def "PBS should validate rules from request when request doesn't contain values"() {
@@ -1333,6 +1449,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1340,11 +1460,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor rules values " +
- "can't be null or empty, but were null"]
+ and: "Response should contain warning"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(PRICE_FLOOR_VALUES_MISSING)]
}
def "PBS should validate rules from request when modelWeight from request is invalid"() {
@@ -1365,6 +1483,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1372,11 +1494,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor modelGroup modelWeight " +
- "must be in range(1-100), but was $invalidModelWeight"]
+ and: "Response should contain warning"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))]
+
where:
invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1]
}
@@ -1404,6 +1525,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest)
+ bidder.setResponse(ampStoredRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAmpRequest(ampRequest)
@@ -1411,11 +1536,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor modelGroup modelWeight " +
- "must be in range(1-100), but was $invalidModelWeight"]
+ and: "Response should contain warning"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))]
where:
invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1]
@@ -1444,6 +1567,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS fetch rules from floors provider"
cacheFloorsProviderRules(bidRequest)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1451,11 +1578,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor root skipRate " +
- "must be in range(0-100), but was $invalidSkipRate"]
+ and: "Response should contain warning"
+ def message = "Price floor root skipRate must be in range(0-100), but was $invalidSkipRate"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
where:
invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
@@ -1484,6 +1610,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS fetch rules from floors provider"
cacheFloorsProviderRules(bidRequest)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1491,11 +1621,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor data skipRate " +
- "must be in range(0-100), but was $invalidSkipRate"]
+ and: "Response should contain warning"
+ def message = "Price floor data skipRate must be in range(0-100), but was $invalidSkipRate"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
where:
invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
@@ -1524,6 +1653,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS fetch rules from floors provider"
cacheFloorsProviderRules(bidRequest)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1531,11 +1664,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor modelGroup skipRate " +
- "must be in range(0-100), but was $invalidSkipRate"]
+ and: "Response should contain warning"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(SKIP_RATE_INVALID.formatted(invalidSkipRate))]
where:
invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
@@ -1560,6 +1691,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1567,11 +1702,230 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
assert bidderRequest.imp[0].bidFloor == floorValue
- and: "Response should contain error"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: Price floor modelGroup default " +
- "must be positive float, but was $invalidDefaultFloorValue"]
+ and: "Response should contain warning"
+ def message = "Price floor modelGroup default must be positive float, but was $invalidDefaultFloorValue"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+ }
+
+ def "PBS shouldn't emit error in log and response when floors is not in request and floors fetching disabled for account"() {
+ given: "Account with disabled fetching"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ config.auction.priceFloors.fetch.enabled = false
+ config.auction.priceFloors.fetch.url = null
+ }
+ accountDao.save(account)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS shouldn't log warning or errors"
+ assert !bidResponse.ext?.warnings
+ assert !bidResponse.ext?.errors
+
+ and: "PBS shouldn't log a errors"
+ def message = "Price floor rules data must be present"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert !floorsLogs.size()
+
+ and: "PBS request on response object status should be in progress"
+ assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == NONE
+
+ and: "PBS bidderRequest status should be in progress"
+ def bidderRequest = bidder.getBidderRequests(bidRequest.id)
+ assert bidderRequest.ext.prebid.floors.fetchStatus == [NONE]
+
+ and: "Alerts.general metrics shouldn't be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert !metrics[ALERT_GENERAL]
+
+ where:
+ bidRequest << [BidRequest.getDefaultBidRequest(), getBidRequestWithFloors().tap { it.ext.prebid.floors = null }]
+ }
+
+ def "PBS should emit error in log and response when floor data is empty and floors fetching disabled for account and #requestFloorEnabled for request"() {
+ given: "Default BidRequest with empty floors.data"
+ def bidRequest = bidRequestWithFloors.tap {
+ it.ext.prebid.floors.enabled = requestFloorEnabled
+ it.ext.prebid.floors.data = null
+ }
+
+ and: "Account with disabled fetching"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ it.config.auction.priceFloors.fetch.enabled = false
+ }
+ accountDao.save(account)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should log a warning"
+ def message = "Price floor rules data must be present"
+ assert bidResponse.ext?.warnings[PREBID]*.code == [999]
+ assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should not add errors"
+ assert !bidResponse.ext.errors
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "PBS request on response object status should be not in progress"
+ assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == NONE
+
+ and: "PBS request status shouldn't be in progress"
+ def bidderRequest = bidder.getBidderRequests(bidRequest.id)
+ assert bidderRequest.ext.prebid.floors.fetchStatus == [NONE]
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+
+ where:
+ requestFloorEnabled << [null, true]
+ }
+
+ def "PBS shouldn't emit error in log and response when floor data is empty and floors fetching disabled for account and floors disabled for request"() {
+ given: "Default BidRequest with empty floors.data"
+ def bidRequest = bidRequestWithFloors.tap {
+ it.ext.prebid.floors.enabled = false
+ it.ext.prebid.floors.data = null
+ }
+
+ and: "Account with disabled fetching"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ it.config.auction.priceFloors.fetch.enabled = false
+ }
+ accountDao.save(account)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS shouldn't log warning or errors"
+ assert !bidResponse.ext?.warnings
+ assert !bidResponse.ext?.errors
+
+ and: "PBS shouldn't log a errors"
+ def message = "Price floor rules data must be present"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert !floorsLogs.size()
+
+ and: "Alerts.general metrics shouldn't be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert !metrics[ALERT_GENERAL]
+ }
+
+ def "PBS shouldn't emit error in log and response when data is invalid and floors fetching enabled for account"() {
+ given: "Default BidRequest with empty floors.data"
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.enabled = requestEnabledFloors
+ ext.prebid.floors.data = null
+ }
+
+ and: "Account with enabled fetching"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId)
+ accountDao.save(account)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS shouldn't log warning or errors"
+ assert !bidResponse.ext?.warnings
+ assert !bidResponse.ext?.errors
+
+ and: "PBS shouldn't log a errors"
+ def message = "Price floor rules data must be present"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert !floorsLogs.size()
+
+ and: "PBS request on response object status should be in progress"
+ assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == INPROGRESS
+
+ and: "PBS bidderRequest status should be in progress"
+ def bidderRequest = bidder.getBidderRequests(bidRequest.id)
+ assert bidderRequest.ext.prebid.floors.fetchStatus == [INPROGRESS]
+
+ and: "Alerts.general metrics shouldn't be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert !metrics[ALERT_GENERAL]
+
+ where:
+ requestEnabledFloors << [null, true]
+ }
+
+ def "PBS shouldn't emit error in log and response when data is invalid and floors disabled for account"() {
+ given: "Default BidRequest with empty floors.data"
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.data = null
+ }
+
+ and: "Account with disabled fetching"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ config.auction.priceFloors.enabled = false
+ config.auction.priceFloors.fetch.enabled = false
+ }
+ accountDao.save(account)
+
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should not add warning or errors"
+ assert !bidResponse.ext.warnings
+ assert !bidResponse.ext.errors
+
+ and: "PBS request on response object status should be empty"
+ assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == null
+
+ and: "PBS request status should be empty"
+ def bidderRequest = bidder.getBidderRequests(bidRequest.id)
+ assert bidderRequest.ext.prebid.floors.fetchStatus.every { it == null }
+
+ and: "Alerts.general metrics shouldn't be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert !metrics[ALERT_GENERAL]
}
def "PBS should not invalidate previously good fetched data when floors provider return invalid data"() {
@@ -1651,7 +2005,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
floorsProvider.setResponse(accountId, floorsResponse)
and: "PBS fetch rules from floors provider"
- cacheFloorsProviderRules(bidRequest)
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1669,11 +2023,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS log should contain error"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" +
- " must be in range(1-100), but was $invalidModelWeight")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, MODEL_WEIGHT_INVALID.formatted(invalidModelWeight)))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -1710,7 +2062,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
floorsProvider.setResponse(accountId, floorsResponse)
and: "PBS fetch rules from floors provider"
- cacheFloorsProviderRules(bidRequest)
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1727,12 +2079,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Price floor data skipRate must be in range(0-100), but was $invalidSkipRate"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor data skipRate" +
- " must be in range(0-100), but was $invalidSkipRate")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -1769,7 +2120,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
floorsProvider.setResponse(accountId, floorsResponse)
and: "PBS fetch rules from floors provider"
- cacheFloorsProviderRules(bidRequest)
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1787,11 +2138,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
and: "PBS log should contain error"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" +
- " must be in range(0-100), but was $invalidSkipRate")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, SKIP_RATE_INVALID.formatted(invalidSkipRate)))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
@@ -1828,7 +2177,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
floorsProvider.setResponse(accountId, floorsResponse)
and: "PBS fetch rules from floors provider"
- cacheFloorsProviderRules(bidRequest)
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
@@ -1845,12 +2194,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
assert metrics[FETCH_FAILURE_METRIC] == 1
and: "PBS log should contain error"
+ def message = "Price floor modelGroup default must be positive float, but was $invalidDefaultFloor"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
+ def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup default" +
- " must be positive float, but was $invalidDefaultFloor")
+ assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message))
and: "Floors validation failure cannot reject the entire auction"
assert !response.seatbid?.isEmpty()
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 5d985596679..96247fe0157 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
@@ -61,6 +61,7 @@ import static org.prebid.server.functional.model.request.auction.FetchStatus.ERR
import static org.prebid.server.functional.model.request.auction.Location.NO_DATA
import static org.prebid.server.functional.model.request.auction.Prebid.Channel
import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR
+import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID
import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
class PriceFloorsRulesSpec extends PriceFloorsBaseSpec {
@@ -217,10 +218,15 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec {
assert bidderRequest.ext?.prebid?.floors?.location == NO_DATA
assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR
- and: "PBS should not contain errors, warnings"
- assert !response.ext?.warnings
+ and: "PBS should not contain errors"
assert !response.ext?.errors
+ and: "PBS should log a warning"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message.first.contains("Cannot deserialize value of type " +
+ "`org.prebid.server.floors.model.PriceFloorField` " +
+ "from String \"bogus\": not one of the values accepted for Enum class")
+
and: "PBS should not reject the entire auction"
assert !response.seatbid.isEmpty()
}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy
index 93f26180213..a885da9e86b 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy
@@ -29,15 +29,20 @@ import static org.prebid.server.functional.model.pricefloors.MediaType.VIDEO
import static org.prebid.server.functional.model.pricefloors.PriceFloorField.MEDIA_TYPE
import static org.prebid.server.functional.model.pricefloors.PriceFloorField.SITE_DOMAIN
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.ErrorType.PREBID
class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
- private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" }
private static final int MAX_SCHEMA_DIMENSIONS_SIZE = 1
private static final int MAX_RULES_SIZE = 1
+ private static Instant startTime
- def "PBS should skip signalling for request with rules when ext.prebid.floors.enabled = false in request"() {
+ def setupSpec() {
+ startTime = Instant.now()
+ }
+
+ def "PBS should skip signaling for request with rules when ext.prebid.floors.enabled = false in request"() {
given: "Default BidRequest with disabled floors"
def bidRequest = bidRequestWithFloors.tap {
ext.prebid.floors.enabled = requestEnabled
@@ -49,24 +54,45 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, SUCCESS)
+
when: "PBS processes auction request"
- floorsPbsService.sendAuctionRequest(bidRequest)
+ def response = floorsPbsService.sendAuctionRequest(bidRequest)
then: "Bidder request bidFloor should correspond request"
- def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+ def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor
assert !bidderRequest.ext?.prebid?.floors?.enabled
and: "PBS should not fetch rules from floors provider"
assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0
+ and: "PBS should not add warning or errors"
+ assert !response.ext.warnings
+ assert !response.ext.errors
+
+ and: "PBS shouldn't log a errors"
+ def message = "Price floor rules data must be present"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert !floorsLogs.size()
+
+ and: "Alerts.general metrics shouldn't be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert !metrics[ALERT_GENERAL]
+
where:
requestEnabled | accountEnabled
false | true
true | false
}
- def "PBS should skip signalling for request without rules when ext.prebid.floors.enabled = false in request"() {
+ def "PBS should skip signaling for request without rules when ext.prebid.floors.enabled = false in request"() {
given: "Default BidRequest"
def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
ext.prebid.floors = new ExtPrebidFloors(enabled: false)
@@ -145,7 +171,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1
}
- def "PBS should not signalling when neither fetched floors nor ext.prebid.floors exist, imp.bidFloor is not defined"() {
+ def "PBS should not signaling when neither fetched floors nor ext.prebid.floors exist, imp.bidFloor is not defined"() {
given: "Default BidRequest"
def bidRequest = BidRequest.defaultBidRequest
@@ -173,7 +199,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1
}
- def "PBS should make PF signalling when skipRate = #skipRate"() {
+ def "PBS should make PF signaling when skipRate = #skipRate"() {
given: "Default BidRequest with bidFloor, bidFloorCur"
def bidRequest = BidRequest.defaultBidRequest.tap {
imp[0].bidFloor = PBSUtils.randomFloorValue
@@ -209,7 +235,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
skipRate << [0, null]
}
- def "PBS should not make PF signalling, enforcing when skipRate = 100"() {
+ def "PBS should not make PF signaling, enforcing when skipRate = 100"() {
given: "Default BidRequest with bidFloor, bidFloorCur"
def bidRequest = BidRequest.defaultBidRequest.tap {
imp[0].bidFloor = 0.8
@@ -243,7 +269,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
assert bidderRequest.ext?.prebid?.floors?.skipRate == 100
assert bidderRequest.ext?.prebid?.floors?.skipped
- and: "PBS should not made signalling"
+ and: "PBS should not made signaling"
assert !bidderRequest.imp[0].ext?.prebid?.floors
where:
@@ -280,7 +306,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should not log warning"
+ then: "PBS should not log warning or errors"
assert !response.ext.warnings
assert !response.ext.errors
@@ -319,8 +345,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
when: "PBS processes amp request"
def response = floorsPbsService.sendAmpRequest(ampRequest)
- then: "PBS should not log warning"
+ then: "PBS should not log warning or errors"
assert !response.ext.warnings
+ assert !response.ext.errors
and: "Bidder request should contain bidFloor from the request"
def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
@@ -520,7 +547,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
assert bidderRequest.imp.last().bidFloor == videoFloorValue
}
- def "PBS shouldn't emit errors when request schema.fields than floor-config.max-schema-dims"() {
+ def "PBS shouldn't emit warning when request schema.fields equal to floor-config.max-schema-dims"() {
given: "Bid request with schema 2 fields"
def bidRequest = bidRequestWithFloors.tap {
ext.prebid.floors.maxSchemaDims = PBSUtils.getRandomNumber(2)
@@ -531,18 +558,23 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
def account = getAccountWithEnabledFetch(accountId)
accountDao.save(account)
- and: "Set bidder response"
+ and: "Default bid response"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
bidder.setResponse(bidRequest.id, bidResponse)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS shouldn't log a errors"
- assert !response.ext?.errors
+ then: "PBS should not add warning or errors"
+ assert !response.ext.warnings
+ assert !response.ext.errors
+
+ and: "Alerts.general metrics shouldn't be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert !metrics[ALERT_GENERAL]
}
- def "PBS should emit errors when request has more rules than price-floor.max-rules"() {
+ def "PBS should emit warning when request has more rules than price-floor.max-rules"() {
given: "BidRequest with 2 rules"
def requestFloorValue = PBSUtils.randomFloorValue
def bidRequest = bidRequestWithFloors.tap {
@@ -551,9 +583,10 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
(new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue]
}
- and: "Account with maxRules in the DB"
+ and: "Account with maxRules and disabled fetching in the DB"
def accountId = bidRequest.site.publisher.id
def account = getAccountWithEnabledFetch(accountId).tap {
+ config.auction.priceFloors.fetch.enabled = false
config.auction.priceFloors.maxRules = maxRules
config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase
}
@@ -564,14 +597,20 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
bidResponse.seatbid.first().bid.first().price = requestFloorValue
bidder.setResponse(bidRequest.id, bidResponse)
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should log a errors"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: " +
- "Price floor rules number ${getRuleSize(bidRequest)} exceeded its maximum number ${MAX_RULES_SIZE}"]
+ then: "PBS should log a warning"
+ def message = "Price floor rules number ${getRuleSize(bidRequest)} exceeded its maximum number ${MAX_RULES_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
where:
maxRules | maxRulesSnakeCase
@@ -579,30 +618,40 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
null | MAX_RULES_SIZE
}
- def "PBS should emit errors when request has more schema.fields than floor-config.max-schema-dims"() {
+ def "PBS should emit warning when request has more schema.fields than floor-config.max-schema-dims"() {
given: "BidRequest with schema 2 fields"
def bidRequest = bidRequestWithFloors
- and: "Account with maxSchemaDims in the DB"
+ and: "Account with maxSchemaDims and disabled fetching in the DB"
def accountId = bidRequest.site.publisher.id
def account = getAccountWithEnabledFetch(accountId).tap {
+ config.auction.priceFloors.fetch.enabled = false
config.auction.priceFloors.maxSchemaDims = maxSchemaDims
config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase
}
accountDao.save(account)
- and: "Set bidder response"
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
bidder.setResponse(bidRequest.id, bidResponse)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should log a errors"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: " +
- "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+ then: "PBS should log a warning"
+ def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
where:
maxSchemaDims | maxSchemaDimsSnakeCase
@@ -610,7 +659,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
null | MAX_SCHEMA_DIMENSIONS_SIZE
}
- def "PBS should emit errors when request has more schema.fields than default-account.max-schema-dims"() {
+ def "PBS should emit warning when request has more schema.fields than default-account.max-schema-dims"() {
given: "Floor config with default account"
def accountConfig = getDefaultAccountConfigSettings().tap {
auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
@@ -627,45 +676,50 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
and: "Account with maxSchemaDims in the DB"
def accountId = bidRequest.site.publisher.id
def account = getAccountWithEnabledFetch(accountId).tap {
+
config.auction.priceFloors.maxSchemaDims = PBSUtils.randomNegativeNumber
}
accountDao.save(account)
- and: "Set bidder response"
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
bidder.setResponse(bidRequest.id, bidResponse)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should log a errors"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: " +
- "Price floor schema dimensions ${getSchemaSize(bidRequest)} " +
- "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+ then: "PBS should log a warning"
+ def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
- and: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Metrics should be updated"
def metrics = floorsPbsService.sendCollectedMetricsRequest()
assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+ assert metrics[ALERT_GENERAL] == 1
cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsFloorConfig)
}
- def "PBS should emit errors when request has more schema.fields than default-account.fetch.max-schema-dims"() {
- given: "Test start time"
- def startTime = Instant.now()
-
- and: "BidRequest with schema 2 fields"
+ def "PBS should emit warning when request has more schema.fields than default-account.fetch.max-schema-dims"() {
+ given: "BidRequest with schema 2 fields"
def bidRequest = bidRequestWithFloors
and: "Floor config with default account"
def accountConfig = getDefaultAccountConfigSettings().tap {
- auction.priceFloors.fetch.enabled = true
+ auction.priceFloors.fetch.enabled = false
auction.priceFloors.fetch.url = BASIC_FETCH_URL + bidRequest.site.publisher.id
auction.priceFloors.fetch.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
- auction.priceFloors.maxSchemaDims = null
+ auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
}
def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled" : "true",
"settings.default-account-config": encode(accountConfig)]
@@ -683,48 +737,67 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
- and: "Set bidder response"
+ and: "Default bid response"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
bidder.setResponse(bidRequest.id, bidResponse)
when: "PBS processes auction request"
- floorsPbsService.sendAuctionRequest(bidRequest)
+ def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should log a errors"
+ then: "Response should includer error warning"
+ def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS shouldn't log a errors"
def logs = floorsPbsService.getLogsByTime(startTime)
- def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
assert floorsLogs.size() == 1
- assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
- "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor schema dimensions ${getSchemaSize(bidRequest)} " +
- "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}")
- and: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+ and: "Metrics should be updated"
def metrics = floorsPbsService.sendCollectedMetricsRequest()
assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+ assert metrics[ALERT_GENERAL] == 1
cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsFloorConfig)
}
- def "PBS should emit errors when request has more schema.fields than fetch.max-schema-dims"() {
- given: "Default BidRequest with floorMin"
+ def "PBS should emit warning when request has more schema.fields than fetch.max-schema-dims"() {
+ given: "BidRequest with schema 2 fields"
def bidRequest = bidRequestWithFloors
and: "Account with disabled fetch in the DB"
def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ config.auction.priceFloors.fetch.enabled = false
config.auction.priceFloors.maxSchemaDims = maxSchemaDims
config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase
}
accountDao.save(account)
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should log a errors"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: " +
- "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+ then: "PBS should log a warning"
+ def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
where:
maxSchemaDims | maxSchemaDimsSnakeCase
@@ -736,28 +809,40 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
given: "BidRequest with schema 2 fields"
def bidRequest = bidRequestWithFloors
- and: "Account with maxSchemaDims in the DB"
+ and: "Account with maxSchemaDims and disabled fetching in the DB"
def accountId = bidRequest.site.publisher.id
def floorSchemaFilesSize = getSchemaSize(bidRequest)
def account = getAccountWithEnabledFetch(accountId).tap {
+ config.auction.priceFloors.fetch.enabled = false
config.auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
config.auction.priceFloors.fetch.maxSchemaDims = floorSchemaFilesSize
}
accountDao.save(account)
- and: "Set bidder response"
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
bidder.setResponse(bidRequest.id, bidResponse)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS should log a errors"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: " +
- "Price floor schema dimensions ${floorSchemaFilesSize} " +
- "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+ then: "PBS should log a warning"
+ def message = "Price floor schema dimensions ${floorSchemaFilesSize} " +
+ "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS shouldn't fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() {
@@ -772,22 +857,22 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
}
accountDao.save(account)
- and: "Set bidder response"
+ and: "Default bid response"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
bidder.setResponse(bidRequest.id, bidResponse)
when: "PBS processes auction request"
def response = floorsPbsService.sendAuctionRequest(bidRequest)
- then: "PBS shouldn't log a errors"
- assert !response.ext?.errors
+ then: "PBS shouldn't add warnings or errors"
+ assert !response.ext?.warnings
}
- def "PBS should emit errors when stored request has more rules than price-floor.max-rules for amp request"() {
+ def "PBS should emit warning when stored request has more rules than price-floor.max-rules for amp request"() {
given: "Default AmpRequest"
def ampRequest = AmpRequest.defaultAmpRequest
- and: "Default stored request with 2 rules "
+ and: "Default stored request with 2 rules"
def requestFloorValue = PBSUtils.randomFloorValue
def ampStoredRequest = BidRequest.defaultStoredRequest.tap {
ext.prebid.floors = ExtPrebidFloors.extPrebidFloors
@@ -798,8 +883,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
storedRequestDao.save(storedRequest)
- and: "Account with maxRules in the DB"
+ and: "Account with maxRules and disabled fetching in the DB"
def account = getAccountWithEnabledFetch(ampRequest.account as String).tap {
+ config.auction.priceFloors.fetch.enabled = false
config.auction.priceFloors.maxRules = maxRules
config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase
}
@@ -810,15 +896,28 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
bidResponse.seatbid.first().bid.first().price = requestFloorValue
bidder.setResponse(ampStoredRequest.id, bidResponse)
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
when: "PBS processes amp request"
def response = floorsPbsService.sendAmpRequest(ampRequest)
- then: "PBS should log a errors"
- assert response.ext?.errors[PREBID]*.code == [999]
- assert response.ext?.errors[PREBID]*.message ==
- ["Failed to parse price floors from request, with a reason: " +
- "Price floor rules number ${getRuleSize(ampStoredRequest)} " +
- "exceeded its maximum number ${MAX_RULES_SIZE}"]
+ then: "PBS should log a warning"
+ def message = "Price floor rules number ${getRuleSize(ampStoredRequest)} " +
+ "exceeded its maximum number ${MAX_RULES_SIZE}"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, "Price Floors can't be resolved for account $ampRequest.account and " +
+ "request $ampStoredRequest.id, reason: Price floors processing failed: Fetching is disabled. " +
+ "Following parsing of request price floors is failed: $message")
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
where:
maxRules | maxRulesSnakeCase
@@ -826,6 +925,219 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
null | MAX_RULES_SIZE
}
+ def "PBS should emit error in log and response when floors skipRate is out of range"() {
+ given: "BidRequest with invalid skipRate"
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.data.skipRate = requestSkipRate
+ }
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ then: "PBS should log a warning"
+ def message = "Price floor data skipRate must be in range(0-100), but was $requestSkipRate"
+ assert bidResponse.ext?.warnings[PREBID]*.code == [999]
+ assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+
+ where:
+ requestSkipRate << [PBSUtils.randomNegativeNumber, PBSUtils.getRandomNumber(100)]
+ }
+
+ def "PBS should emit error in log and response when floors modelGroups is empty"() {
+ given: "BidRequest with empty modelGroups"
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.data.modelGroups = requestModelGroups
+ }
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should log a warning"
+ def message = "Price floor rules should contain at least one model group"
+ assert bidResponse.ext?.warnings[PREBID]*.code == [999]
+ assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+
+ where:
+ requestModelGroups << [null, []]
+ }
+
+ def "PBS should emit error in log and response when modelGroup modelWeight is out of range"() {
+ given: "BidRequest with invalid modelWeight"
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.data.modelGroups = [
+ new FloorModelGroup(modelWeight: requestModelWeight)
+ ]
+ }
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should log a warning"
+ def message = "Price floor modelGroup modelWeight must be in range(1-100), but was $requestModelWeight"
+ assert bidResponse.ext?.warnings[PREBID]*.code == [999]
+ assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+
+ where:
+ requestModelWeight << [PBSUtils.randomNegativeNumber, PBSUtils.getRandomNumber(100)]
+ }
+
+ def "PBS should emit error in log and response when modelGroup skipRate is out of range"() {
+ given: "BidRequest with invalid modelGroup skipRate"
+ def requestModelGroupsSkipRate = PBSUtils.getRandomNumber(100)
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.data.modelGroups = [
+ new FloorModelGroup(skipRate: requestModelGroupsSkipRate)
+ ]
+ }
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should log a warning"
+ def message = "Price floor modelGroup skipRate must be in range(0-100), but was $requestModelGroupsSkipRate"
+ assert bidResponse.ext?.warnings[PREBID]*.code == [999]
+ assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log an errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+ }
+
+ def "PBS should emit error in log and response when modelGroup defaultFloor is negative"() {
+ given: "BidRequest with negative defaultFloor"
+ def requestModelGroupsSkipRate = PBSUtils.randomNegativeNumber
+
+ def bidRequest = bidRequestWithFloors.tap {
+ ext.prebid.floors.data.modelGroups = [
+ new FloorModelGroup(defaultFloor: requestModelGroupsSkipRate)
+ ]
+ }
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ and: "Default bid response"
+ def response = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, response)
+
+ when: "PBS processes auction request"
+ def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should log a warning"
+ def message = "Price floor modelGroup default must be positive float, but was $requestModelGroupsSkipRate"
+ assert bidResponse.ext?.warnings[PREBID]*.code == [999]
+ assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+ }
+
+ def "PBS should emit error in log and response when account have disabled dynamic data config"() {
+ given: "Bid request without floors"
+ def bidRequest = getBidRequestWithFloors().tap {
+ ext.prebid.floors.data = null
+ }
+
+ and: "Account with disabled dynamic data"
+ def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+ config.auction.priceFloors.useDynamicData = false
+ }
+ accountDao.save(account)
+
+ and: "PBS fetch rules from floors provider"
+ cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, SUCCESS)
+
+ and: "Default bid response"
+ def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+ bidder.setResponse(bidRequest.id, bidResponse)
+
+ and: "Flush metrics"
+ flushMetrics(floorsPbsService)
+
+ when: "PBS processes auction request"
+ def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+ then: "PBS should log a warning"
+ def message = "Price floor rules data must be present"
+ assert response.ext?.warnings[PREBID]*.code == [999]
+ assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)]
+
+ and: "PBS should log a errors"
+ def logs = floorsPbsService.getLogsByTime(startTime)
+ def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, 'Using dynamic data is not allowed', message))
+ assert floorsLogs.size() == 1
+
+ and: "Alerts.general metrics should be populated"
+ def metrics = floorsPbsService.sendCollectedMetricsRequest()
+ assert metrics[ALERT_GENERAL] == 1
+ }
+
private static int getSchemaSize(BidRequest bidRequest) {
bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].schema.fields.size()
}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy
index 771fac9bd84..717c9f32d5b 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy
@@ -394,7 +394,7 @@ class GdprAmpSpec extends PrivacyBaseSpec {
and: "Alerts.general metrics should be populated"
def metrics = privacyPbsService.sendCollectedMetricsRequest()
- assert metrics["alerts.general"] == 1
+ assert metrics[ALERT_GENERAL] == 1
and: "Bidder should be called"
assert bidder.getBidderRequest(ampStoredRequest.id)
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 7302ad79aed..5fc0f5d7bca 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
@@ -342,7 +342,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
and: "Alerts.general metrics should be populated"
def metrics = privacyPbsService.sendCollectedMetricsRequest()
- assert metrics["alerts.general"] == 1
+ assert metrics[ALERT_GENERAL] == 1
and: "Bid response should contain seatBid"
assert response.seatbid.size() == 1
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy
index 46634fda48f..2dd15697102 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy
@@ -60,7 +60,6 @@ import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT
import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT
-import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL
import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT
import static org.prebid.server.functional.model.request.GppSectionId.USP_V1
@@ -694,7 +693,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
where:
gppAccountsConfig << [[new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false),
@@ -868,7 +867,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when custom privacy regulation with normalizing should ignore call to bidder"() {
@@ -1425,7 +1424,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when privacy module contain invalid property should respond with an error"() {
@@ -1624,7 +1623,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when custom privacy regulation with normalizing should ignore call to bidder"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy
index a6d9d6e7aad..c95acb92475 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy
@@ -59,7 +59,6 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection
import static org.prebid.server.functional.model.pricefloors.Country.CAN
import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT
-import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL
import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT
import static org.prebid.server.functional.model.request.GppSectionId.USP_V1
@@ -624,7 +623,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS cookie sync call when privacy module contain invalid code should include proper responded with bidders URLs"() {
@@ -784,7 +783,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS cookie sync when custom privacy regulation with normalizing should exclude bidders URLs"() {
@@ -1482,7 +1481,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS setuid request call when privacy module contain invalid code should respond with valid bidders UIDs cookies"() {
@@ -1657,7 +1656,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS setuid call when custom privacy regulation with normalizing should reject bidders with status code invalidStatusCode"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy
index 73dfda85a23..a825f10c287 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy
@@ -59,7 +59,6 @@ import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT
import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT
-import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL
import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT
import static org.prebid.server.functional.model.request.GppSectionId.USP_V1
@@ -983,7 +982,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when privacy module contain invalid property should respond with an error"() {
@@ -1145,7 +1144,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when custom privacy regulation with normalizing that match custom config should have empty EIDS fields"() {
@@ -2008,7 +2007,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when privacy module contain invalid property should respond with an error"() {
@@ -2206,7 +2205,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy
index c0c9495bfc3..4707633d01d 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy
@@ -58,7 +58,6 @@ import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT
import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT
-import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL
import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT
import static org.prebid.server.functional.model.request.GppSectionId.USP_V1
@@ -1359,7 +1358,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when privacy module contain invalid code should respond with an error"() {
@@ -1573,7 +1572,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when custom privacy regulation with normalizing should change request consent and call to bidder"() {
@@ -2613,7 +2612,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when privacy module contain invalid code should respond with an error"() {
@@ -2863,7 +2862,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy
index 920196ea576..1dc12037a23 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy
@@ -69,7 +69,6 @@ import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT
import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT
-import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL
import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT
import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT
import static org.prebid.server.functional.model.request.GppSectionId.USP_V1
@@ -1330,7 +1329,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when privacy module contain invalid property should respond with an error"() {
@@ -1525,7 +1524,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS auction call when custom privacy regulation with normalizing that match custom config should have empty UFPD fields"() {
@@ -2637,7 +2636,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when privacy module contain invalid property should respond with an error"() {
@@ -2868,7 +2867,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec {
and: "Metrics for disallowed activities should be updated"
def metrics = activityPbsService.sendCollectedMetricsRequest()
- assert metrics[ALERT_GENERAL.getValue()] == 1
+ assert metrics[ALERT_GENERAL] == 1
}
def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() {
diff --git a/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java b/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java
index 872a04c0a96..f69fb0f5415 100644
--- a/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java
@@ -6,6 +6,7 @@
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.request.Native;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.Video;
import com.iab.openrtb.response.Bid;
@@ -21,12 +22,15 @@
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderCall;
import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.CompositeBidderResponse;
import org.prebid.server.bidder.model.HttpRequest;
import org.prebid.server.bidder.model.HttpResponse;
import org.prebid.server.bidder.model.Result;
import org.prebid.server.currency.CurrencyConversionService;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.epsilon.ExtImpEpsilon;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
import java.math.BigDecimal;
import java.util.Collections;
@@ -513,6 +517,65 @@ public void makeHttpRequestsShouldPrioritizeVideoProtocolsFromImpExtEvenIfInvali
.isEmpty();
}
+ @Test
+ public void makeHttpRequestsShouldSetImpBidFloorFromImpExtIfPresentAndImpBidFloorIsInvalid() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ impBuilder -> impBuilder.bidfloor(BigDecimal.valueOf(-1.00)),
+ extBuilder -> extBuilder.bidfloor(BigDecimal.ONE));
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+ .flatExtracting(BidRequest::getImp)
+ .extracting(Imp::getBidfloor)
+ .containsExactly(BigDecimal.ONE);
+ }
+
+ @Test
+ public void makeHttpRequestsShouldNotSetImpBidFloorFromImpExt() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ impBuilder -> impBuilder.bidfloor(BigDecimal.valueOf(-1.00)),
+ extBuilder -> extBuilder.bidfloor(BigDecimal.valueOf(-2.00)));
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+ .flatExtracting(BidRequest::getImp)
+ .extracting(Imp::getBidfloor)
+ .containsExactly(BigDecimal.valueOf(-1.00));
+ }
+
+ @Test
+ public void makeHttpRequestsShouldReturnConvertedBidFloorCurrency() {
+ // given
+ given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
+ .willReturn(BigDecimal.ONE);
+
+ final BidRequest bidRequest = givenBidRequest(
+ impBuilder -> impBuilder.bidfloor(BigDecimal.TEN).bidfloorcur("EUR"));
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue())
+ .extracting(HttpRequest::getPayload)
+ .flatExtracting(BidRequest::getImp)
+ .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
+ .containsOnly(AssertionsForClassTypes.tuple(BigDecimal.ONE, "USD"));
+ }
+
@Test
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
// given
@@ -654,62 +717,55 @@ public void makeBidsShouldUpdateBidWithUUIDIfGenerateBidIdIsTrue() throws JsonPr
}
@Test
- public void makeHttpRequestsShouldSetImpBidFloorFromImpExtIfPresentAndImpBidFloorIsInvalid() {
+ public void makeBidsShouldReturnResultForNativeBidsWithExpectedFields() throws JsonProcessingException {
// given
- final BidRequest bidRequest = givenBidRequest(
- impBuilder -> impBuilder.bidfloor(BigDecimal.valueOf(-1.00)),
- extBuilder -> extBuilder.bidfloor(BigDecimal.ONE));
-
- // when
- final Result>> result = target.makeHttpRequests(bidRequest);
-
- // then
- assertThat(result.getErrors()).isEmpty();
- assertThat(result.getValue()).hasSize(1)
- .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
- .flatExtracting(BidRequest::getImp)
- .extracting(Imp::getBidfloor)
- .containsExactly(BigDecimal.ONE);
- }
-
- @Test
- public void makeHttpRequestsShouldNotSetImpBidFloorFromImpExt() {
- // given
- final BidRequest bidRequest = givenBidRequest(
- impBuilder -> impBuilder.bidfloor(BigDecimal.valueOf(-1.00)),
- extBuilder -> extBuilder.bidfloor(BigDecimal.valueOf(-2.00)));
-
- // when
- final Result>> result = target.makeHttpRequests(bidRequest);
-
- // then
- assertThat(result.getErrors()).isEmpty();
- assertThat(result.getValue()).hasSize(1)
- .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
- .flatExtracting(BidRequest::getImp)
- .extracting(Imp::getBidfloor)
- .containsExactly(BigDecimal.valueOf(-1.00));
- }
-
- @Test
- public void makeHttpRequestsShouldReturnConvertedBidFloorCurrency() {
- // given
- given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
- .willReturn(BigDecimal.ONE);
+ final String nativeRequestString =
+ "{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":80}}";
+ final String nativeResponseString =
+ "\"native\"{\"assets\": [{\"id\": 1, \"title\": {\"text\": \"Native test (Title)\"}}], "
+ + "\"link\": {\"url\": \"https://www.epsilon.com/\"}, "
+ + "\"imptrackers\":[\"https://iad-usadmm.dotomi.com/event\"],\"jstracker\":\"\"}";
+ final BidRequest bidRequest = BidRequest.builder()
+ .id("native-test")
+ .imp(singletonList(Imp.builder()
+ .id("impid-0")
+ .xNative(Native.builder()
+ .request(nativeRequestString)
+ .ver("1.2")
+ .build())
+ .build()))
+ .build();
- final BidRequest bidRequest = givenBidRequest(
- impBuilder -> impBuilder.bidfloor(BigDecimal.TEN).bidfloorcur("EUR"));
+ final BidderCall httpCall = givenHttpCall(bidRequest,
+ mapper.writeValueAsString(BidResponse.builder()
+ .seatbid(singletonList(SeatBid.builder()
+ .bid(singletonList(Bid.builder()
+ .price(BigDecimal.ONE)
+ .impid("impid-0")
+ .adm(nativeResponseString)
+ .mtype(4)
+ .cat(singletonList("IAB3"))
+ .build()))
+ .build()))
+ .cur("USD")
+ .ext(ExtBidResponse.builder().build())
+ .build()));
// when
- final Result>> result = target.makeHttpRequests(bidRequest);
+ final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest);
// then
assertThat(result.getErrors()).isEmpty();
- assertThat(result.getValue())
- .extracting(HttpRequest::getPayload)
- .flatExtracting(BidRequest::getImp)
- .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
- .containsOnly(AssertionsForClassTypes.tuple(BigDecimal.ONE, "USD"));
+ assertThat(result.getBids()).hasSize(1)
+ .containsOnly(BidderBid.of(
+ Bid.builder()
+ .impid("impid-0")
+ .price(BigDecimal.ONE)
+ .adm(nativeResponseString)
+ .cat(singletonList("IAB3"))
+ .mtype(4)
+ .build(),
+ BidType.xNative, "USD"));
}
private static BidRequest givenBidRequest(
diff --git a/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java b/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java
new file mode 100644
index 00000000000..886af999d74
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java
@@ -0,0 +1,221 @@
+package org.prebid.server.bidder.zeta_global_ssp;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.response.Bid;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.VertxTest;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderCall;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.HttpRequest;
+import org.prebid.server.bidder.model.HttpResponse;
+import org.prebid.server.bidder.model.Result;
+import org.prebid.server.bidder.theadx.TheadxBidder;
+import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp.ExtImpZetaGlobalSSP;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+
+import java.util.List;
+import java.util.function.UnaryOperator;
+
+import static java.util.Collections.singletonList;
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.prebid.server.bidder.model.BidderError.Type.bad_server_response;
+import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER;
+import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE;
+import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER;
+import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
+
+public class ZetaGlobalSspBidderTest extends VertxTest {
+
+ private static final String ENDPOINT_URL = "https://test-url.com/{{AccountID}}";
+
+ private final ZetaGlobalSspBidder target = new ZetaGlobalSspBidder(ENDPOINT_URL, jacksonMapper);
+
+ @Test
+ public void shouldFailOnBidderCreation() {
+ assertThatIllegalArgumentException().isThrownBy(() -> new TheadxBidder("invalid_url", jacksonMapper));
+ }
+
+ @Test
+ public void makeHttpRequestsShouldReturnErrorIfImpExtMissing() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ givenImp(imp -> imp.id("imp1")
+ .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))));
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> {
+ assertThat(error.getMessage()).contains("Missing bidder ext in impression with id: imp1");
+ assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
+ });
+ assertThat(result.getValue()).isEmpty();
+ }
+
+ @Test
+ public void makeHttpRequestsShouldCreateSingleRequestAndRemoveImpExt() {
+ // given
+ final Imp imp1 = givenImp(imp -> imp.id("imp1").ext(givenImpExt(11)));
+ final Imp imp2 = givenImp(imp -> imp.id("imp2").ext(givenImpExt(44)));
+ final BidRequest bidRequest = givenBidRequest(imp1, imp2);
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+ final HttpRequest httpRequest = result.getValue().getFirst();
+
+ // then
+ assertThat(result.getValue())
+ .extracting(HttpRequest::getPayload)
+ .flatExtracting(BidRequest::getImp)
+ .extracting(Imp::getExt)
+ .containsExactly(null, givenImpExt(44));
+ assertThat(result.getErrors()).isEmpty();
+ }
+
+ @Test
+ public void makeHttpRequestsShouldReturnExpectedHeaders() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(givenImp(identity()));
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getValue()).hasSize(1).first()
+ .extracting(HttpRequest::getHeaders)
+ .satisfies(headers -> {
+ assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE);
+ assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE);
+ });
+ assertThat(result.getErrors()).isEmpty();
+ }
+
+ @Test
+ public void makeHttpRequestsShouldResolveMacroInEndpointUrl() {
+ // given
+ final Imp imp1 = givenImp(imp -> imp.id("imp1").ext(givenImpExt(11)));
+ final BidRequest bidRequest = givenBidRequest(imp1);
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getUri)
+ .containsExactly("https://test-url.com/11");
+ }
+
+ @Test
+ public void makeBidsShouldReturnErrorIfResponseBodyInvalid() {
+ // given
+ final BidderCall httpCall = givenHttpCall("invalid-response-body");
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> {
+ assertThat(error.getMessage()).contains("Failed to decode:");
+ assertThat(error.getType()).isEqualTo(bad_server_response);
+ });
+ }
+
+ @Test
+ public void makeBidsShouldReturnEmptyListIfSeatBidIsNullOrEmpty() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall =
+ givenHttpCall(mapper.writeValueAsString(BidResponse.builder().cur("USD").build()));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).isEmpty();
+ }
+
+ @Test
+ public void makeBidsShouldReturnErrorIfCannotResolveBidType() throws JsonProcessingException {
+ // given
+ final Bid bid = givenBid("imp1", mapper.createObjectNode());
+ final BidderCall httpCall =
+ givenHttpCall(mapper.writeValueAsString(givenBidResponse(List.of(bid))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> {
+ assertThat(error.getMessage()).contains("Failed to parse impression \"imp1\" mediatype");
+ assertThat(error.getType()).isEqualTo(bad_server_response);
+ });
+ }
+
+ @Test
+ public void makeBidsShouldReturnBannerBidIfTypeParsedProperly() throws JsonProcessingException {
+ // given
+ final ObjectNode extWithPrebidType = mapper.createObjectNode();
+ extWithPrebidType.putObject("prebid").put("type", "banner");
+ final Bid validBid = givenBid("imp1", extWithPrebidType);
+
+ final BidResponse bidResponse = givenBidResponse(List.of(validBid));
+ final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1);
+ final BidderBid bidderBid = result.getValue().getFirst();
+ assertThat(bidderBid.getBid().getImpid()).isEqualTo("imp1");
+ assertThat(bidderBid.getType()).isEqualTo(BidType.banner);
+ assertThat(bidderBid.getBidCurrency()).isEqualTo("USD");
+ }
+
+ private static BidRequest givenBidRequest(Imp... imps) {
+ return BidRequest.builder().imp(List.of(imps)).build();
+ }
+
+ private static Imp givenImp(UnaryOperator impCustomizer) {
+ return impCustomizer.apply(Imp.builder().id("imp_id").ext(givenImpExt(11))).build();
+ }
+
+ private static ObjectNode givenImpExt(Integer sid) {
+ return mapper.valueToTree(ExtPrebid.of(null, ExtImpZetaGlobalSSP.of(sid)));
+ }
+
+ private static BidderCall givenHttpCall(String body) {
+ return BidderCall.succeededHttp(
+ HttpRequest.builder().payload(null).build(),
+ HttpResponse.of(200, null, body),
+ null);
+ }
+
+ private static Bid givenBid(String impId, ObjectNode ext) {
+ return Bid.builder().impid(impId).ext(ext).build();
+ }
+
+ private static BidResponse givenBidResponse(List bids) {
+ return BidResponse.builder()
+ .cur("USD")
+ .seatbid(singletonList(SeatBid.builder().bid(bids).build()))
+ .build();
+ }
+
+}
diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java
index 78ce82dd3fd..f70471b1b4d 100644
--- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java
+++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java
@@ -20,6 +20,8 @@
import org.prebid.server.floors.model.PriceFloorSchema;
import org.prebid.server.floors.proto.FetchResult;
import org.prebid.server.floors.proto.FetchStatus;
+import org.prebid.server.metric.MetricName;
+import org.prebid.server.metric.Metrics;
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
@@ -51,12 +53,14 @@ public class BasicPriceFloorProcessorTest extends VertxTest {
private PriceFloorFetcher priceFloorFetcher;
@Mock
private PriceFloorResolver floorResolver;
+ @Mock
+ private Metrics metrics;
private BasicPriceFloorProcessor target;
@BeforeEach
public void setUp() {
- target = new BasicPriceFloorProcessor(priceFloorFetcher, floorResolver, jacksonMapper);
+ target = new BasicPriceFloorProcessor(priceFloorFetcher, floorResolver, metrics, jacksonMapper, 0.0d);
}
@Test
@@ -70,7 +74,7 @@ public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForAccount() {
new ArrayList<>());
// then
- verifyNoInteractions(priceFloorFetcher);
+ verifyNoInteractions(priceFloorFetcher, metrics);
assertThat(result).isEqualTo(givenBidRequest(identity(), PriceFloorRules.builder().enabled(false).build()));
}
@@ -85,7 +89,7 @@ public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForRequest() {
new ArrayList<>());
// then
- verifyNoInteractions(priceFloorFetcher);
+ verifyNoInteractions(priceFloorFetcher, metrics);
assertThat(result)
.extracting(BidRequest::getExt)
@@ -98,8 +102,8 @@ public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForRequest() {
@Test
public void shouldUseFloorsDataFromProviderIfPresent() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -118,16 +122,16 @@ public void shouldUseFloorsDataFromProviderIfPresent() {
.data(providerFloorsData)
.fetchStatus(FetchStatus.success)
.location(PriceFloorLocation.fetch)));
+ verifyNoInteractions(metrics);
+
}
@Test
public void shouldUseFloorsFromProviderIfUseDynamicDataAndUseFetchDataRateAreAbsent() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
- .floorProvider("provider.com")
- .useFetchDataRate(null));
+ final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.useFetchDataRate(null));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -145,16 +149,15 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataAndUseFetchDataRateAreAbs
.data(providerFloorsData)
.fetchStatus(FetchStatus.success)
.location(PriceFloorLocation.fetch)));
+ verifyNoInteractions(metrics);
}
@Test
public void shouldUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs100() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
- .floorProvider("provider.com")
- .useFetchDataRate(100));
+ final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.useFetchDataRate(100));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -172,16 +175,15 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRa
.data(providerFloorsData)
.fetchStatus(FetchStatus.success)
.location(PriceFloorLocation.fetch)));
+ verifyNoInteractions(metrics);
}
@Test
public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs0() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
- .floorProvider("provider.com")
- .useFetchDataRate(0));
+ final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.useFetchDataRate(0));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -199,15 +201,44 @@ public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDat
assertThat(actualRules)
.extracting(PriceFloorRules::getLocation)
.isEqualTo(PriceFloorLocation.noData);
+ verifyNoInteractions(metrics);
}
@Test
- public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrueAndUseFetchDataRateIsAbsent() {
+ public void shouldTolerateInvalidFloorsFromRequestWhenFetchIsSuccessAndUseFetchDataRateIs0() {
// given
final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
.floorProvider("provider.com")
- .useFetchDataRate(null));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ .useFetchDataRate(0));
+
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
+ final ArrayList warnings = new ArrayList<>();
+
+ // when
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.data(null))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.success)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+
+ assertThat(warnings).isEmpty();
+ verifyNoInteractions(metrics);
+ }
+
+ @Test
+ public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrueAndUseFetchDataRateIsAbsent() {
+ // given
+ final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.useFetchDataRate(null));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -226,65 +257,330 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrueAndUseFetchDataRate
.floorMin(BigDecimal.ONE)
.fetchStatus(FetchStatus.success)
.location(PriceFloorLocation.fetch)));
+ verifyNoInteractions(metrics);
}
@Test
- public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIsAbsent() {
+ public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
- .floorProvider("provider.com")
- .useFetchDataRate(null));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
+ final ArrayList warnings = new ArrayList<>();
final BidRequest result = target.enrichWithPriceFloors(
givenBidRequest(identity(), null),
givenAccount(floorsConfig -> floorsConfig.useDynamicData(false)),
"bidder",
new ArrayList<>(),
- new ArrayList<>());
+ warnings);
// then
- final PriceFloorRules actualRules = extractFloors(result);
- assertThat(actualRules)
- .extracting(PriceFloorRules::getFetchStatus)
- .isEqualTo(FetchStatus.success);
- assertThat(actualRules)
- .extracting(PriceFloorRules::getLocation)
- .isEqualTo(PriceFloorLocation.noData);
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.success)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
}
@Test
- public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIs100() {
+ public void shouldNotTolerateInvalidFloorsFromRequestWhenFetchIsSuccessAndUseDynamicDataIsFalse() {
// given
final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
.floorProvider("provider.com")
- .useFetchDataRate(100));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ .useFetchDataRate(0));
+
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
+ final ArrayList warnings = new ArrayList<>();
// when
final BidRequest result = target.enrichWithPriceFloors(
- givenBidRequest(identity(), null),
+ givenBidRequest(identity(), givenFloors(floors -> floors.data(null))),
givenAccount(floorsConfig -> floorsConfig.useDynamicData(false)),
"bidder",
new ArrayList<>(),
- new ArrayList<>());
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.success)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+
+ verify(metrics).updateAlertsMetrics(MetricName.general);
+ assertThat(warnings).containsExactly("Price floors processing failed: "
+ + "parsing of request price floors is failed: Price floor rules data must be present");
+ }
+
+ @Test
+ public void shouldUseFloorsFromRequestIfUseDynamicDataIsFalse() {
+ // given
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.floorMin(BigDecimal.ONE))),
+ givenAccount(floorsConfig -> floorsConfig.useDynamicData(false)),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
+ .enabled(true)
+ .skipped(false)
+ .fetchStatus(FetchStatus.success)
+ .floorMin(BigDecimal.ONE)
+ .location(PriceFloorLocation.request)));
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
+ }
+
+ @Test
+ public void shouldNotUseFloorsWhenProviderFetchingIsDisabled() {
+ // given
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any()))
+ .willReturn(FetchResult.of(providerFloorsData, FetchStatus.none, "errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), null),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.none)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
+ }
+
+ @Test
+ public void shouldNotTolerateInvalidFloorsFromRequestWhenFetchIsDisabled() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.data(null))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.none)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+ verify(metrics).updateAlertsMetrics(MetricName.general);
+ assertThat(warnings).containsExactly("Price floors processing failed: "
+ + "parsing of request price floors is failed: Price floor rules data must be present");
+ }
+
+ @Test
+ public void shouldNotUseFloorsWhenProviderFetchingIsFailed() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.error("errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), null),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.error)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
+ }
+
+ @Test
+ public void shouldNotTolerateInvalidFloorsFromRequestWhenFetchIsFailed() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.error("errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.data(null))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.error)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+ verify(metrics).updateAlertsMetrics(MetricName.general);
+ assertThat(warnings).containsExactly("Price floors processing failed: "
+ + "parsing of request price floors is failed: Price floor rules data must be present");
+ }
+
+ @Test
+ public void shouldNotUseFloorsWhenProviderFetchingIsFailedWithTimeout() {
+ // given
+ given(priceFloorFetcher.fetch(any()))
+ .willReturn(FetchResult.of(null, FetchStatus.timeout, "errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), null),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.timeout)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
+ }
+
+ @Test
+ public void shouldNotTolerateInvalidFloorsFromRequestWhenFetchIsFailedWithTimeout() {
+ // given
+ given(priceFloorFetcher.fetch(any()))
+ .willReturn(FetchResult.of(null, FetchStatus.timeout, "errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.data(null))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
// then
final PriceFloorRules actualRules = extractFloors(result);
assertThat(actualRules)
.extracting(PriceFloorRules::getFetchStatus)
- .isEqualTo(FetchStatus.success);
+ .isEqualTo(FetchStatus.timeout);
assertThat(actualRules)
.extracting(PriceFloorRules::getLocation)
.isEqualTo(PriceFloorLocation.noData);
+ verify(metrics).updateAlertsMetrics(MetricName.general);
+ assertThat(warnings).containsExactly("Price floors processing failed: "
+ + "parsing of request price floors is failed: Price floor rules data must be present");
+ }
+
+ @Test
+ public void shouldUseFloorsFromRequestIfFetchingIsDisabled() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.floorMin(BigDecimal.ONE))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
+ .enabled(true)
+ .skipped(false)
+ .fetchStatus(FetchStatus.none)
+ .floorMin(BigDecimal.ONE)
+ .location(PriceFloorLocation.request)));
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
+ }
+
+ @Test
+ public void shouldUseFloorsFromRequestIfFetchingIsFailed() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.error("errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.floorMin(BigDecimal.ONE))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
+ .enabled(true)
+ .skipped(false)
+ .fetchStatus(FetchStatus.error)
+ .floorMin(BigDecimal.ONE)
+ .location(PriceFloorLocation.request)));
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
+ }
+
+ @Test
+ public void shouldUseFloorsFromRequestIfFetchingIsFailedWithTimeout() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(null, FetchStatus.timeout, "errorMessage"));
+
+ // when
+ final ArrayList warnings = new ArrayList<>();
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.floorMin(BigDecimal.ONE))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
+ .enabled(true)
+ .skipped(false)
+ .fetchStatus(FetchStatus.timeout)
+ .floorMin(BigDecimal.ONE)
+ .location(PriceFloorLocation.request)));
+ verifyNoInteractions(metrics);
+ assertThat(warnings).isEmpty();
}
@Test
public void shouldMergeProviderWithRequestFloors() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -315,8 +611,8 @@ public void shouldMergeProviderWithRequestFloors() {
@Test
public void shouldReturnProviderFloorsWhenNotEnabledByRequestAndEnforceRateAndFloorPriceAreAbsent() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -348,8 +644,8 @@ public void shouldReturnFloorsWithFloorMinAndCurrencyFromRequestWhenPresent() {
.floorMin(BigDecimal.ONE)
.data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("USD"))));
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -368,7 +664,7 @@ public void shouldReturnFloorsWithFloorMinAndCurrencyFromRequestWhenPresent() {
@Test
public void shouldUseFloorsFromRequestIfProviderFloorsMissing() {
// given
- given(priceFloorFetcher.fetch(any())).willReturn(null);
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -380,17 +676,19 @@ public void shouldUseFloorsFromRequestIfProviderFloorsMissing() {
// then
assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
- .enabled(true)
- .skipped(false)
- .floorMin(BigDecimal.ONE)
- .location(PriceFloorLocation.request)));
+ .fetchStatus(FetchStatus.none)
+ .enabled(true)
+ .skipped(false)
+ .floorMin(BigDecimal.ONE)
+ .location(PriceFloorLocation.request)));
+ verifyNoInteractions(metrics);
}
@Test
public void shouldTolerateUsingFloorsFromRequestWhenRulesNumberMoreThanMaxRulesNumber() {
// given
- given(priceFloorFetcher.fetch(any())).willReturn(null);
- final ArrayList errors = new ArrayList<>();
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
+ final ArrayList warnings = new ArrayList<>();
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -404,25 +702,27 @@ public void shouldTolerateUsingFloorsFromRequestWhenRulesNumberMoreThanMaxRulesN
)),
givenAccount(floorConfigBuilder -> floorConfigBuilder.maxRules(1L)),
"bidder",
- errors,
- new ArrayList<>());
+ new ArrayList<>(),
+ warnings);
// then
assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.none)
.enabled(true)
.skipped(false)
.location(PriceFloorLocation.noData)
.build());
- assertThat(errors).containsOnly("Failed to parse price floors from request, with a reason: "
+ assertThat(warnings).containsOnly("Price floors processing failed: "
+ + "parsing of request price floors is failed: "
+ "Price floor rules number 2 exceeded its maximum number 1");
}
@Test
public void shouldTolerateUsingFloorsFromRequestWhenDimensionsNumberMoreThanMaxDimensionsNumber() {
// given
- given(priceFloorFetcher.fetch(any())).willReturn(null);
- final ArrayList errors = new ArrayList<>();
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
+ final ArrayList warnings = new ArrayList<>();
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -436,24 +736,52 @@ public void shouldTolerateUsingFloorsFromRequestWhenDimensionsNumberMoreThanMaxD
)),
givenAccount(floorConfigBuilder -> floorConfigBuilder.maxSchemaDims(1L)),
"bidder",
- errors,
- new ArrayList<>());
+ new ArrayList<>(),
+ warnings);
// then
assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.none)
.enabled(true)
.skipped(false)
.location(PriceFloorLocation.noData)
.build());
- assertThat(errors).containsOnly("Failed to parse price floors from request, with a reason: "
+ assertThat(warnings).containsOnly("Price floors processing failed: "
+ + "parsing of request price floors is failed: "
+ "Price floor schema dimensions 2 exceeded its maximum number 1");
}
+ @Test
+ public void shouldTolerateInvalidFloorsFromRequestWhenFetchIsInProgress() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.inProgress());
+ final ArrayList warnings = new ArrayList<>();
+
+ // when
+ final BidRequest result = target.enrichWithPriceFloors(
+ givenBidRequest(identity(), givenFloors(floors -> floors.data(null))),
+ givenAccount(identity()),
+ "bidder",
+ new ArrayList<>(),
+ warnings);
+
+ // then
+ assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+ .fetchStatus(FetchStatus.inprogress)
+ .enabled(true)
+ .skipped(false)
+ .location(PriceFloorLocation.noData)
+ .build());
+
+ assertThat(warnings).isEmpty();
+ verifyNoInteractions(metrics);
+ }
+
@Test
public void shouldTolerateMissingRequestAndProviderFloors() {
// given
- given(priceFloorFetcher.fetch(any())).willReturn(null);
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -475,6 +803,9 @@ public void shouldTolerateMissingRequestAndProviderFloors() {
@Test
public void shouldNotSkipFloorsIfRootSkipRateIsOff() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
+
// when
final BidRequest result = target.enrichWithPriceFloors(
givenBidRequest(identity(), givenFloors(floors -> floors.skipRate(0))),
@@ -486,6 +817,7 @@ public void shouldNotSkipFloorsIfRootSkipRateIsOff() {
// then
assertThat(extractFloors(result))
.isEqualTo(givenFloors(floors -> floors
+ .fetchStatus(FetchStatus.none)
.enabled(true)
.skipped(false)
.skipRate(0)
@@ -494,6 +826,9 @@ public void shouldNotSkipFloorsIfRootSkipRateIsOff() {
@Test
public void shouldSkipFloorsIfRootSkipRateIsOn() {
+ // given
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
+
// when
final BidRequest result = target.enrichWithPriceFloors(
givenBidRequest(identity(), givenFloors(floors -> floors.skipRate(100))),
@@ -504,16 +839,18 @@ public void shouldSkipFloorsIfRootSkipRateIsOn() {
// then
assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
- .skipRate(100)
- .enabled(true)
- .skipped(true)
- .location(PriceFloorLocation.request)));
+ .fetchStatus(FetchStatus.none)
+ .skipRate(100)
+ .enabled(true)
+ .skipped(true)
+ .location(PriceFloorLocation.request)));
}
@Test
public void shouldSkipFloorsIfDataSkipRateIsOn() {
// given
final PriceFloorData priceFloorData = givenFloorData(floorData -> floorData.skipRate(100));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -525,11 +862,13 @@ public void shouldSkipFloorsIfDataSkipRateIsOn() {
// then
assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
- .enabled(true)
- .skipRate(100)
- .data(priceFloorData)
- .skipped(true)
- .location(PriceFloorLocation.request)));
+ .fetchStatus(FetchStatus.none)
+ .enabled(true)
+ .skipRate(100)
+ .floorProvider("provider.com")
+ .data(priceFloorData)
+ .skipped(true)
+ .location(PriceFloorLocation.request)));
}
@Test
@@ -538,6 +877,7 @@ public void shouldSkipFloorsIfModelGroupSkipRateIsOn() {
final PriceFloorData priceFloorData = givenFloorData(floorData -> floorData
.skipRate(0)
.modelGroups(singletonList(givenModelGroup(group -> group.skipRate(100)))));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -549,11 +889,13 @@ public void shouldSkipFloorsIfModelGroupSkipRateIsOn() {
// then
assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
- .data(priceFloorData)
- .skipRate(100)
- .enabled(true)
- .skipped(true)
- .location(PriceFloorLocation.request)));
+ .fetchStatus(FetchStatus.none)
+ .data(priceFloorData)
+ .skipRate(100)
+ .floorProvider("provider.com")
+ .enabled(true)
+ .skipped(true)
+ .location(PriceFloorLocation.request)));
}
@Test
@@ -562,6 +904,7 @@ public void shouldNotUpdateImpsIfSelectedModelGroupIsMissing() {
final List imps = singletonList(givenImp(identity()));
final PriceFloorRules requestFloors = givenFloors(floors -> floors
.data(givenFloorData(floorData -> floorData.modelGroups(null))));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -582,6 +925,7 @@ public void shouldUseSelectedModelGroup() {
final PriceFloorModelGroup modelGroup = givenModelGroup(identity());
final PriceFloorRules requestFloors = givenFloors(floors -> floors
.data(givenFloorData(floorData -> floorData.modelGroups(singletonList(modelGroup)))));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
// when
target.enrichWithPriceFloors(
@@ -603,8 +947,8 @@ public void shouldUseSelectedModelGroup() {
@Test
public void shouldCopyFloorProviderValueFromDataLevel() {
// given
- final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
- given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+ final PriceFloorData providerFloorsData = givenFloorData(identity());
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success, null));
// when
final BidRequest result = target.enrichWithPriceFloors(
@@ -630,6 +974,7 @@ public void shouldNotUpdateImpsIfBidFloorNotResolved() {
.data(givenFloorData(floorData -> floorData
.modelGroups(singletonList(givenModelGroup(identity()))))));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
given(floorResolver.resolve(any(), any(), any(), eq("bidder"), any())).willReturn(null);
// when
@@ -660,6 +1005,7 @@ public void shouldUpdateImpsIfBidFloorResolved() {
final List imps = singletonList(givenImp(impBuilder -> impBuilder.ext(givenImpExt)));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
given(floorResolver.resolve(any(), any(), any(), eq("bidder"), any()))
.willReturn(PriceFloorResult.of("rule", BigDecimal.ONE, BigDecimal.TEN, "USD"));
@@ -692,6 +1038,7 @@ public void shouldTolerateFloorResolvingError() {
final PriceFloorRules requestFloors = givenFloors(floors -> floors
.data(givenFloorData(floorData -> floorData.modelGroups(singletonList(givenModelGroup(identity()))))));
+ given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.none("errorMessage"));
given(floorResolver.resolve(any(), any(), any(), eq("bidder"), any()))
.willThrow(new IllegalStateException("error"));
@@ -712,6 +1059,7 @@ private static Account givenAccount(
UnaryOperator floorsConfigCustomizer) {
return Account.builder()
+ .id("accountId")
.auction(AccountAuctionConfig.builder()
.priceFloors(floorsConfigCustomizer.apply(AccountPriceFloorsConfig.builder()).build())
.build())
@@ -722,6 +1070,7 @@ private static BidRequest givenBidRequest(UnaryOperator floorsCustomizer) {
return floorsCustomizer.apply(PriceFloorRules.builder()
+ .enabled(true)
.data(PriceFloorData.builder()
.modelGroups(singletonList(PriceFloorModelGroup.builder()
.value("someKey", BigDecimal.ONE)
@@ -750,6 +1100,7 @@ private static PriceFloorData givenFloorData(
UnaryOperator floorDataCustomizer) {
return floorDataCustomizer.apply(PriceFloorData.builder()
+ .floorProvider("provider.com")
.modelGroups(singletonList(PriceFloorModelGroup.builder()
.value("someKey", BigDecimal.ONE)
.schema(PriceFloorSchema.of("|", List.of(size)))
diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java
index 4fe2e31180b..542559bd862 100644
--- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java
+++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java
@@ -104,8 +104,8 @@ public void fetchShouldReturnPriceFloorFetchedFromProviderAndCache() {
verifyNoMoreInteractions(httpClient);
final FetchResult priceFloorRulesCached = priceFloorFetcher.fetch(givenAccount);
- assertThat(priceFloorRulesCached.getFetchStatus()).isEqualTo(FetchStatus.success);
- assertThat(priceFloorRulesCached.getRulesData()).isEqualTo(givenPriceFloorData());
+ assertThat(priceFloorRulesCached)
+ .isEqualTo(FetchResult.of(givenPriceFloorData(), FetchStatus.success, null));
}
@@ -119,8 +119,7 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocation(
final FetchResult fetchResult = priceFloorFetcher.fetch(givenAccount(identity()));
// then
- assertThat(fetchResult.getRulesData()).isNull();
- assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(fetchResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
}
@@ -134,13 +133,13 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocationA
final FetchResult firstInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
// then
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.error(
+ "Failed to fetch price floor from provider for fetch.url "
+ + "'http://test.host.com', with a reason: failed"));
}
@Test
@@ -158,8 +157,10 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocationA
verify(vertx).setTimer(eq(1200000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.timeout);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.of(
+ null,
+ FetchStatus.timeout,
+ "Fetch price floor request timeout for fetch.url 'http://test.host.com' exceeded."));
}
@Test
@@ -317,8 +318,8 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsMalformedAndReturnErro
// then
verifyNoInteractions(httpClient);
- assertThat(fetchResult.getRulesData()).isNull();
- assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(fetchResult).isEqualTo(FetchResult.error(
+ "Malformed fetch.url 'MalformedURl' passed"));
verifyNoInteractions(vertx);
}
@@ -329,8 +330,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsBlankAndReturnErrorSta
// then
verifyNoInteractions(httpClient);
- assertThat(fetchResult.getRulesData()).isNull();
- assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(fetchResult).isEqualTo(FetchResult.error("Malformed fetch.url ' ' passed"));
verifyNoInteractions(vertx);
}
@@ -341,8 +341,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsNotProvidedAndReturnEr
// then
verifyNoInteractions(httpClient);
- assertThat(fetchResult.getRulesData()).isNull();
- assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(fetchResult).isEqualTo(FetchResult.error("Malformed fetch.url 'null' passed"));
verifyNoInteractions(vertx);
}
@@ -353,8 +352,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchEnabledIsFalseAndReturnNone
// then
verifyNoInteractions(httpClient);
- assertThat(fetchResult.getRulesData()).isNull();
- assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.none);
+ assertThat(fetchResult).isEqualTo(FetchResult.none("Fetching is disabled"));
verifyNoInteractions(vertx);
}
@@ -370,13 +368,13 @@ public void fetchShouldReturnEmptyRulesAndErrorStatusForSecondCallAndCreatePerio
// then
verify(httpClient).get(anyString(), anyLong(), anyLong());
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
verify(vertx).setTimer(eq(1500000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.error(
+ "Failed to fetch price floor from provider for fetch.url 'http://test.host.com', "
+ + "with a reason: Failed to request, provider respond with status 400"));
verifyNoMoreInteractions(vertx);
}
@@ -392,13 +390,16 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusAndCreatePeriodicTimerWhen
// then
verify(httpClient).get(anyString(), anyLong(), anyLong());
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
verify(vertx).setTimer(eq(1500000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
assertThat(secondInvocationResult.getRulesData()).isNull();
assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult.getErrorMessage()).startsWith(
+ "Failed to fetch price floor from provider for fetch.url 'http://test.host.com', "
+ + "with a reason: Failed to parse price floor response, "
+ + "cause: DecodeException: Failed to decode:");
verifyNoMoreInteractions(vertx);
}
@@ -414,13 +415,14 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusForSecondCallAndCreatePeri
// then
verify(httpClient).get(anyString(), anyLong(), anyLong());
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
verify(vertx).setTimer(eq(1500000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.error(
+ "Failed to fetch price floor from provider for fetch.url 'http://test.host.com', "
+ + "with a reason: Failed to parse price floor response, "
+ + "response body can not be empty"));
verifyNoMoreInteractions(vertx);
}
@@ -436,13 +438,14 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusForSecondCallAndCreatePeri
// then
verify(httpClient).get(anyString(), anyLong(), anyLong());
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
verify(vertx).setTimer(eq(1500000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.error(
+ "Failed to fetch price floor from provider for fetch.url 'http://test.host.com', "
+ + "with a reason: Failed to parse price floor response, "
+ + "response body can not be empty"));
verifyNoMoreInteractions(vertx);
}
@@ -460,8 +463,7 @@ public void fetchShouldNotCallPriceFloorProviderWhileFetchIsAlreadyInProgress()
verify(httpClient).get(anyString(), anyLong(), anyLong());
verifyNoMoreInteractions(httpClient);
- assertThat(secondFetch.getRulesData()).isNull();
- assertThat(secondFetch.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(secondFetch).isEqualTo(FetchResult.inProgress());
fetchPromise.tryComplete(
HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap()
@@ -490,13 +492,13 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules
// then
verify(httpClient).get(anyString(), anyLong(), anyLong());
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
verify(vertx).setTimer(eq(1500000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.error(
+ "Failed to fetch price floor from provider for fetch.url 'http://test.host.com', "
+ + "with a reason: Price floor rules number 2 exceeded its maximum number 1"));
verifyNoMoreInteractions(vertx);
}
@@ -518,13 +520,13 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededDimen
// then
verify(httpClient).get(anyString(), anyLong(), anyLong());
- assertThat(firstInvocationResult.getRulesData()).isNull();
- assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+ assertThat(firstInvocationResult).isEqualTo(FetchResult.inProgress());
verify(vertx).setTimer(eq(1200000L), any());
verify(vertx).setTimer(eq(1500000L), any());
final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
- assertThat(secondInvocationResult.getRulesData()).isNull();
- assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+ assertThat(secondInvocationResult).isEqualTo(FetchResult.error(
+ "Failed to fetch price floor from provider for fetch.url 'http://test.host.com', "
+ + "with a reason: Price floor rules values can't be null or empty, but were {}"));
verifyNoMoreInteractions(vertx);
}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json
index 0a3824d6e31..e68be13ed77 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json
@@ -12,7 +12,9 @@
]
},
"ext": {
- "zeta_global_ssp": {}
+ "zeta_global_ssp": {
+ "sid": 11
+ }
}
}
],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json
index 2608812c09e..d982ec42345 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json
@@ -11,11 +11,7 @@
}
]
},
- "secure": 1,
- "ext": {
- "tid": "${json-unit.any-string}",
- "bidder": {}
- }
+ "secure": 1
}
],
"site": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json
index c31fabcb822..39d74ae42cd 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json
@@ -12,9 +12,15 @@
"cid": "cid001",
"adm": "adm001",
"h": 250,
- "w": 300
+ "w": 300,
+ "ext": {
+ "prebid": {
+ "type": "banner"
+ }
+ }
}
- ]
+ ],
+ "seat": "zeta_global_ssp"
}
]
}
diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties
index 450ae419cb5..a19968c87c2 100644
--- a/src/test/resources/org/prebid/server/it/test-application.properties
+++ b/src/test/resources/org/prebid/server/it/test-application.properties
@@ -14,8 +14,6 @@ adapters.generic.aliases.cwire.enabled=true
adapters.generic.aliases.cwire.endpoint=http://localhost:8090/cwire-exchange
adapters.generic.aliases.infytv.enabled=true
adapters.generic.aliases.infytv.endpoint=http://localhost:8090/infytv-exchange
-adapters.generic.aliases.zeta_global_ssp.enabled=true
-adapters.generic.aliases.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange
adapters.aceex.enabled=true
adapters.aceex.endpoint=http://localhost:8090/aceex-exchange
adapters.acuityads.enabled=true
@@ -581,6 +579,8 @@ adapters.yieldone.enabled=true
adapters.yieldone.endpoint=http://localhost:8090/yieldone-exchange
adapters.zeroclickfraud.enabled=true
adapters.zeroclickfraud.endpoint=http://{{Host}}/zeroclickfraud-exchange?sid={{SourceId}}
+adapters.zeta_global_ssp.enabled=true
+adapters.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange
adapters.aax.enabled=true
adapters.aax.endpoint=http://localhost:8090/aax-exchange
adapters.zmaticoo.enabled=true